diff --git a/.clang-format b/.clang-format
index 7548f76b9b80cc6c1725505ab0492be1d7e3317a..22e04bab0e95d05981218e51cd6affb85f82a45f 100644
--- a/.clang-format
+++ b/.clang-format
@@ -1,6 +1,6 @@
 AccessModifierOffset: -4
 
-AlignAfterOpenBracket: DontAlign
+AlignAfterOpenBracket: Align
 AlignConsecutiveAssignments: false
 AlignConsecutiveDeclarations: false
 AlignConsecutiveMacros: true
@@ -9,10 +9,10 @@ AlignOperands: false
 AlignTrailingComments: true
 
 AllowAllArgumentsOnNextLine: true
-AllowAllConstructorInitializersOnNextLine: true
+AllowAllConstructorInitializersOnNextLine: false
 AllowAllParametersOfDeclarationOnNextLine: false
 AllowShortBlocksOnASingleLine: Empty
-AllowShortCaseLabelsOnASingleLine: true
+AllowShortCaseLabelsOnASingleLine: false
 AllowShortFunctionsOnASingleLine: Empty
 AllowShortIfStatementsOnASingleLine: Never
 AllowShortLambdasOnASingleLine: Inline
@@ -56,7 +56,7 @@ CommentPragmas: ''
 
 CompactNamespaces: false
 
-ConstructorInitializerAllOnOneLineOrOnePerLine: true
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
 ConstructorInitializerIndentWidth: 4
 
 ContinuationIndentWidth: 4
diff --git a/.gitignore b/.gitignore
index 0bdf5476787722ba2007a85c8c73255d56178262..c1e09d2e36074cd6cea7aaf6edc06becf5a826c3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,33 +1,36 @@
-.vs/
-.vscode/
-build*/
-bin*/
-logs/
-dist/
-CMakeLists.txt.user*
-*.autosave
-*.creator
-*.creator.user*
-\#*\#
-/.emacs.desktop
-/.emacs.desktop.lock
-*.elc
-auto-save-list
-tramp
-.\#*
-*~
-.fuse_hudden*
-.directory
-.Trash-*
-.nfs*
-Thumbs.db
-Thumbs.db:encryptable
-ehthumbs.db
-ehthumbs_vista.db
-$RECYCLE.BIN/
-*.stackdump
-[Dd]esktop.ini
-*.egg-info
-__pycache__/
-env/
-venv/
\ No newline at end of file
+.vs/
+.vscode/
+build*/
+bin*/
+doc/
+dist/
+CMakeLists.txt.user*
+*.autosave
+*.creator
+*.creator.user*
+\#*\#
+/.emacs.desktop
+/.emacs.desktop.lock
+*.elc
+auto-save-list
+tramp
+.\#*
+*~
+.fuse_hudden*
+.directory
+.Trash-*
+.nfs*
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+$RECYCLE.BIN/
+*.stackdump
+[Dd]esktop.ini
+*.egg-info
+__pycache__/
+env/
+venv/
+*.pyd
+*.so
+_b_asic_debug_log.txt
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3471fb4f473b069f9a4f77efec5d7aeb0fc84a79..3f8304e375e1442932b0cf79e2855505c74690d3 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2,17 +2,19 @@ cmake_minimum_required(VERSION 3.8)
 
 project(
 	"B-ASIC"
-	VERSION 0.0.1
-	DESCRIPTION "Better ASIC Toolbox for python3"
+	VERSION 1.0.0
+	DESCRIPTION "Better ASIC Toolbox for Python 3"
 	LANGUAGES C CXX
 )
 
-find_package(fmt 5.2.1 REQUIRED)
+# Find dependencies.
+find_package(fmt REQUIRED)
 find_package(pybind11 CONFIG REQUIRED)
 
-set(LIBRARY_NAME "b_asic")
-set(TARGET_NAME "_${LIBRARY_NAME}")
+set(LIBRARY_NAME "b_asic") # Name of the python library directory.
+set(TARGET_NAME "_${LIBRARY_NAME}") # Name of this extension module.
 
+# Set output directory for compiled binaries.
 if(NOT CMAKE_LIBRARY_OUTPUT_DIRECTORY)
 	include(GNUInstallDirs)
 	set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_INSTALL_LIBDIR}")
@@ -29,22 +31,42 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
 
+# Add files to be compiled into Python module.
 pybind11_add_module(
 	"${TARGET_NAME}"
+
+	# Main files.
 	"${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp"
+	"${CMAKE_CURRENT_SOURCE_DIR}/src/simulation.cpp"
+	
+	# For DOD simulation.
+	"${CMAKE_CURRENT_SOURCE_DIR}/src/simulation/compile.cpp"
+	"${CMAKE_CURRENT_SOURCE_DIR}/src/simulation/run.cpp"
+	"${CMAKE_CURRENT_SOURCE_DIR}/src/simulation/simulation.cpp"
+
+	# For OOP simulation (see legacy folder).
+	#"${CMAKE_CURRENT_SOURCE_DIR}/src/simulation/custom_operation.cpp"
+	#"${CMAKE_CURRENT_SOURCE_DIR}/src/simulation/operation.cpp"
+	#"${CMAKE_CURRENT_SOURCE_DIR}/src/simulation/signal_flow_graph.cpp"
+	#"${CMAKE_CURRENT_SOURCE_DIR}/src/simulation/simulation.cpp"
+	#"${CMAKE_CURRENT_SOURCE_DIR}/src/simulation/special_operations.cpp"
 )
 
+# Include headers.
 target_include_directories(
 	"${TARGET_NAME}"
     PRIVATE
         "${CMAKE_CURRENT_SOURCE_DIR}/src"
 )
 
+# Use C++17.
 target_compile_features(
 	"${TARGET_NAME}"
 	PRIVATE
 		cxx_std_17
 )
+
+# Set compiler-specific options using generator expressions.
 target_compile_options(
 	"${TARGET_NAME}"
 	PRIVATE
@@ -60,20 +82,20 @@ target_compile_options(
 		>
 )
 
+# Add libraries. Note: pybind11 is already added in pybind11_add_module.
 target_link_libraries(
 	"${TARGET_NAME}"
 	PRIVATE
-		fmt::fmt
+		$<TARGET_NAME_IF_EXISTS:fmt::fmt-header-only>
+		$<$<NOT:$<TARGET_EXISTS:fmt::fmt-header-only>>:fmt::fmt>
 )
 
-add_custom_target(
-	remove_old_python_dir ALL
-	COMMAND ${CMAKE_COMMAND} -E remove_directory "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${LIBRARY_NAME}"
-	COMMENT "Removing old python directory ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${LIBRARY_NAME}"
-)
-add_custom_target(
-	copy_python_dir ALL
-	COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_LIST_DIR}/${LIBRARY_NAME}" "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${LIBRARY_NAME}"
-	COMMENT "Copying python files to ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${LIBRARY_NAME}"
-	DEPENDS "${TARGET_NAME}" remove_old_python_dir
-)
\ No newline at end of file
+# Copy binaries to project folder for debugging during development.
+if(NOT ASIC_BUILDING_PYTHON_DISTRIBUTION)
+	add_custom_target(
+		copy_binaries ALL
+		COMMAND ${CMAKE_COMMAND} -E copy "$<TARGET_FILE:${TARGET_NAME}>" "${CMAKE_CURRENT_LIST_DIR}"
+		COMMENT "Copying binaries to ${CMAKE_CURRENT_LIST_DIR}"
+		DEPENDS "${TARGET_NAME}"
+	)
+endif()
diff --git a/Doxyfile b/Doxyfile
new file mode 100644
index 0000000000000000000000000000000000000000..6ee7c4a5c8fbda53c512e45642de09d3ef65ff16
--- /dev/null
+++ b/Doxyfile
@@ -0,0 +1,2553 @@
+# Doxyfile 1.8.18
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project.
+#
+# All text after a double hash (##) is considered a comment and is placed in
+# front of the TAG it is preceding.
+#
+# All text after a single hash (#) is considered a comment and will be ignored.
+# The format is:
+# TAG = value [value, ...]
+# For lists, items can also be appended using:
+# TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (\" \").
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# This tag specifies the encoding used for all characters in the configuration
+# file that follow. The default is UTF-8 which is also the encoding used for all
+# text before the first occurrence of this tag. Doxygen uses libiconv (or the
+# iconv built into libc) for the transcoding. See
+# https://www.gnu.org/software/libiconv/ for the list of possible encodings.
+# The default value is: UTF-8.
+
+DOXYFILE_ENCODING      = UTF-8
+
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by
+# double-quotes, unless you are using Doxywizard) that should identify the
+# project for which the documentation is generated. This name is used in the
+# title of most generated pages and in a few other places.
+# The default value is: My Project.
+
+PROJECT_NAME           = "B-ASIC"
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
+# could be handy for archiving the generated documentation or if some version
+# control system is used.
+
+PROJECT_NUMBER         =
+
+# Using the PROJECT_BRIEF tag one can provide an optional one line description
+# for a project that appears at the top of each page and should give viewer a
+# quick idea about the purpose of the project. Keep the description short.
+
+PROJECT_BRIEF          = "Better ASIC Toolbox for Python 3"
+
+# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
+# in the documentation. The maximum height of the logo should not exceed 55
+# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
+# the logo to the output directory.
+
+PROJECT_LOGO           = "logo_tiny.png"
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
+# into which the generated documentation will be written. If a relative path is
+# entered, it will be relative to the location where doxygen was started. If
+# left blank the current directory will be used.
+
+OUTPUT_DIRECTORY       = "doc"
+
+# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
+# directories (in 2 levels) under the output directory of each output format and
+# will distribute the generated files over these directories. Enabling this
+# option can be useful when feeding doxygen a huge amount of source files, where
+# putting all generated files in the same directory would otherwise causes
+# performance problems for the file system.
+# The default value is: NO.
+
+CREATE_SUBDIRS         = NO
+
+# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
+# characters to appear in the names of generated files. If set to NO, non-ASCII
+# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
+# U+3044.
+# The default value is: NO.
+
+ALLOW_UNICODE_NAMES    = NO
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,
+# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),
+# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,
+# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),
+# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,
+# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,
+# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,
+# Ukrainian and Vietnamese.
+# The default value is: English.
+
+OUTPUT_LANGUAGE        = English
+
+# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all generated output in the proper direction.
+# Possible values are: None, LTR, RTL and Context.
+# The default value is: None.
+
+OUTPUT_TEXT_DIRECTION  = None
+
+# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
+# descriptions after the members that are listed in the file and class
+# documentation (similar to Javadoc). Set to NO to disable this.
+# The default value is: YES.
+
+BRIEF_MEMBER_DESC      = YES
+
+# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief
+# description of a member or function before the detailed description
+#
+# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+# The default value is: YES.
+
+REPEAT_BRIEF           = YES
+
+# This tag implements a quasi-intelligent brief description abbreviator that is
+# used to form the text in various listings. Each string in this list, if found
+# as the leading text of the brief description, will be stripped from the text
+# and the result, after processing the whole list, is used as the annotated
+# text. Otherwise, the brief description is used as-is. If left blank, the
+# following values are used ($name is automatically replaced with the name of
+# the entity):The $name class, The $name widget, The $name file, is, provides,
+# specifies, contains, represents, a, an and the.
+
+ABBREVIATE_BRIEF       = "The $name class" \
+                         "The $name widget" \
+                         "The $name file" \
+                         is \
+                         provides \
+                         specifies \
+                         contains \
+                         represents \
+                         a \
+                         an \
+                         the
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# doxygen will generate a detailed section even if there is only a brief
+# description.
+# The default value is: NO.
+
+ALWAYS_DETAILED_SEC    = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
+# inherited members of a class in the documentation of that class as if those
+# members were ordinary class members. Constructors, destructors and assignment
+# operators of the base classes will not be shown.
+# The default value is: NO.
+
+INLINE_INHERITED_MEMB  = NO
+
+# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path
+# before files name in the file list and in the header files. If set to NO the
+# shortest path that makes the file name unique will be used
+# The default value is: YES.
+
+FULL_PATH_NAMES        = YES
+
+# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.
+# Stripping is only done if one of the specified strings matches the left-hand
+# part of the path. The tag can be used to show relative paths in the file list.
+# If left blank the directory from which doxygen is run is used as the path to
+# strip.
+#
+# Note that you can specify absolute paths here, but also relative paths, which
+# will be relative from the directory where doxygen is started.
+# This tag requires that the tag FULL_PATH_NAMES is set to YES.
+
+STRIP_FROM_PATH        =
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the
+# path mentioned in the documentation of a class, which tells the reader which
+# header file to include in order to use a class. If left blank only the name of
+# the header file containing the class definition is used. Otherwise one should
+# specify the list of include paths that are normally passed to the compiler
+# using the -I flag.
+
+STRIP_FROM_INC_PATH    =
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but
+# less readable) file names. This can be useful is your file systems doesn't
+# support long names like on DOS, Mac, or CD-ROM.
+# The default value is: NO.
+
+SHORT_NAMES            = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the
+# first line (until the first dot) of a Javadoc-style comment as the brief
+# description. If set to NO, the Javadoc-style will behave just like regular Qt-
+# style comments (thus requiring an explicit @brief command for a brief
+# description.)
+# The default value is: NO.
+
+JAVADOC_AUTOBRIEF      = NO
+
+# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line
+# such as
+# /***************
+# as being the beginning of a Javadoc-style comment "banner". If set to NO, the
+# Javadoc-style will behave just like regular comments and it will not be
+# interpreted by doxygen.
+# The default value is: NO.
+
+JAVADOC_BANNER         = NO
+
+# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first
+# line (until the first dot) of a Qt-style comment as the brief description. If
+# set to NO, the Qt-style will behave just like regular Qt-style comments (thus
+# requiring an explicit \brief command for a brief description.)
+# The default value is: NO.
+
+QT_AUTOBRIEF           = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a
+# multi-line C++ special comment block (i.e. a block of //! or /// comments) as
+# a brief description. This used to be the default behavior. The new default is
+# to treat a multi-line C++ comment block as a detailed description. Set this
+# tag to YES if you prefer the old behavior instead.
+#
+# Note that setting this tag to YES also means that rational rose comments are
+# not recognized any more.
+# The default value is: NO.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the
+# documentation from any documented member that it re-implements.
+# The default value is: YES.
+
+INHERIT_DOCS           = YES
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new
+# page for each member. If set to NO, the documentation of a member will be part
+# of the file/class/namespace that contains it.
+# The default value is: NO.
+
+SEPARATE_MEMBER_PAGES  = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen
+# uses this value to replace tabs by spaces in code fragments.
+# Minimum value: 1, maximum value: 16, default value: 4.
+
+TAB_SIZE               = 4
+
+# This tag can be used to specify a number of aliases that act as commands in
+# the documentation. An alias has the form:
+# name=value
+# For example adding
+# "sideeffect=@par Side Effects:\n"
+# will allow you to put the command \sideeffect (or @sideeffect) in the
+# documentation, which will result in a user-defined paragraph with heading
+# "Side Effects:". You can put \n's in the value part of an alias to insert
+# newlines (in the resulting output). You can put ^^ in the value part of an
+# alias to insert a newline as if a physical newline was in the original file.
+# When you need a literal { or } or , in the value part of an alias you have to
+# escape them by means of a backslash (\), this can lead to conflicts with the
+# commands \{ and \} for these it is advised to use the version @{ and @} or use
+# a double escape (\\{ and \\})
+
+ALIASES                =
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
+# only. Doxygen will then generate output that is more tailored for C. For
+# instance, some of the names that are used will be different. The list of all
+# members will be omitted, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_FOR_C  = NO
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
+# Python sources only. Doxygen will then generate output that is more tailored
+# for that language. For instance, namespaces will be presented as packages,
+# qualified scopes will look different, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_JAVA   = YES
+
+# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
+# sources. Doxygen will then generate output that is tailored for Fortran.
+# The default value is: NO.
+
+OPTIMIZE_FOR_FORTRAN   = NO
+
+# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
+# sources. Doxygen will then generate output that is tailored for VHDL.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_VHDL   = NO
+
+# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice
+# sources only. Doxygen will then generate output that is more tailored for that
+# language. For instance, namespaces will be presented as modules, types will be
+# separated into more groups, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_SLICE  = NO
+
+# Doxygen selects the parser to use depending on the extension of the files it
+# parses. With this tag you can assign which parser to use for a given
+# extension. Doxygen has a built-in mapping, but you can override or extend it
+# using this tag. The format is ext=language, where ext is a file extension, and
+# language is one of the parsers supported by doxygen: IDL, Java, JavaScript,
+# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, VHDL,
+# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran:
+# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser
+# tries to guess whether the code is fixed or free formatted code, this is the
+# default for Fortran type files). For instance to make doxygen treat .inc files
+# as Fortran files (default is PHP), and .f files as C (default is Fortran),
+# use: inc=Fortran f=C.
+#
+# Note: For files without extension you can use no_extension as a placeholder.
+#
+# Note that for custom extensions you also need to set FILE_PATTERNS otherwise
+# the files are not read by doxygen.
+
+EXTENSION_MAPPING      =
+
+# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
+# according to the Markdown format, which allows for more readable
+# documentation. See https://daringfireball.net/projects/markdown/ for details.
+# The output of markdown processing is further processed by doxygen, so you can
+# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in
+# case of backward compatibilities issues.
+# The default value is: YES.
+
+MARKDOWN_SUPPORT       = YES
+
+# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up
+# to that level are automatically included in the table of contents, even if
+# they do not have an id attribute.
+# Note: This feature currently applies only to Markdown headings.
+# Minimum value: 0, maximum value: 99, default value: 5.
+# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.
+
+TOC_INCLUDE_HEADINGS   = 5
+
+# When enabled doxygen tries to link words that correspond to documented
+# classes, or namespaces to their corresponding documentation. Such a link can
+# be prevented in individual cases by putting a % sign in front of the word or
+# globally by setting AUTOLINK_SUPPORT to NO.
+# The default value is: YES.
+
+AUTOLINK_SUPPORT       = YES
+
+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
+# to include (a tag file for) the STL sources as input, then you should set this
+# tag to YES in order to let doxygen match functions declarations and
+# definitions whose arguments contain STL classes (e.g. func(std::string);
+# versus func(std::string) {}). This also make the inheritance and collaboration
+# diagrams that involve STL classes more complete and accurate.
+# The default value is: NO.
+
+BUILTIN_STL_SUPPORT    = NO
+
+# If you use Microsoft's C++/CLI language, you should set this option to YES to
+# enable parsing support.
+# The default value is: NO.
+
+CPP_CLI_SUPPORT        = NO
+
+# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:
+# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen
+# will parse them like normal C++ but will assume all classes use public instead
+# of private inheritance when no explicit protection keyword is present.
+# The default value is: NO.
+
+SIP_SUPPORT            = NO
+
+# For Microsoft's IDL there are propget and propput attributes to indicate
+# getter and setter methods for a property. Setting this option to YES will make
+# doxygen to replace the get and set methods by a property in the documentation.
+# This will only work if the methods are indeed getting or setting a simple
+# type. If this is not the case, or you want to show the methods anyway, you
+# should set this option to NO.
+# The default value is: YES.
+
+IDL_PROPERTY_SUPPORT   = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+# The default value is: NO.
+
+DISTRIBUTE_GROUP_DOC   = NO
+
+# If one adds a struct or class to a group and this option is enabled, then also
+# any nested class or struct is added to the same group. By default this option
+# is disabled and one has to add nested compounds explicitly via \ingroup.
+# The default value is: NO.
+
+GROUP_NESTED_COMPOUNDS = NO
+
+# Set the SUBGROUPING tag to YES to allow class member groups of the same type
+# (for instance a group of public functions) to be put as a subgroup of that
+# type (e.g. under the Public Functions section). Set it to NO to prevent
+# subgrouping. Alternatively, this can be done per class using the
+# \nosubgrouping command.
+# The default value is: YES.
+
+SUBGROUPING            = YES
+
+# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions
+# are shown inside the group in which they are included (e.g. using \ingroup)
+# instead of on a separate page (for HTML and Man pages) or section (for LaTeX
+# and RTF).
+#
+# Note that this feature does not work in combination with
+# SEPARATE_MEMBER_PAGES.
+# The default value is: NO.
+
+INLINE_GROUPED_CLASSES = NO
+
+# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions
+# with only public data fields or simple typedef fields will be shown inline in
+# the documentation of the scope in which they are defined (i.e. file,
+# namespace, or group documentation), provided this scope is documented. If set
+# to NO, structs, classes, and unions are shown on a separate page (for HTML and
+# Man pages) or section (for LaTeX and RTF).
+# The default value is: NO.
+
+INLINE_SIMPLE_STRUCTS  = NO
+
+# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or
+# enum is documented as struct, union, or enum with the name of the typedef. So
+# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
+# with name TypeT. When disabled the typedef will appear as a member of a file,
+# namespace, or class. And the struct will be named TypeS. This can typically be
+# useful for C code in case the coding convention dictates that all compound
+# types are typedef'ed and only the typedef is referenced, never the tag name.
+# The default value is: NO.
+
+TYPEDEF_HIDES_STRUCT   = NO
+
+# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This
+# cache is used to resolve symbols given their name and scope. Since this can be
+# an expensive process and often the same symbol appears multiple times in the
+# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small
+# doxygen will become slower. If the cache is too large, memory is wasted. The
+# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range
+# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536
+# symbols. At the end of a run doxygen will report the cache usage and suggest
+# the optimal cache size from a speed point of view.
+# Minimum value: 0, maximum value: 9, default value: 0.
+
+LOOKUP_CACHE_SIZE      = 0
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in
+# documentation are documented, even if no documentation was available. Private
+# class members and static file members will be hidden unless the
+# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.
+# Note: This will also disable the warnings about undocumented members that are
+# normally produced when WARNINGS is set to YES.
+# The default value is: NO.
+
+EXTRACT_ALL            = NO
+
+# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
+# be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PRIVATE        = NO
+
+# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual
+# methods of a class will be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PRIV_VIRTUAL   = NO
+
+# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal
+# scope will be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PACKAGE        = NO
+
+# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be
+# included in the documentation.
+# The default value is: NO.
+
+EXTRACT_STATIC         = NO
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
+# locally in source files will be included in the documentation. If set to NO,
+# only classes defined in header files are included. Does not have any effect
+# for Java sources.
+# The default value is: YES.
+
+EXTRACT_LOCAL_CLASSES  = YES
+
+# This flag is only useful for Objective-C code. If set to YES, local methods,
+# which are defined in the implementation section but not in the interface are
+# included in the documentation. If set to NO, only methods in the interface are
+# included.
+# The default value is: NO.
+
+EXTRACT_LOCAL_METHODS  = NO
+
+# If this flag is set to YES, the members of anonymous namespaces will be
+# extracted and appear in the documentation as a namespace called
+# 'anonymous_namespace{file}', where file will be replaced with the base name of
+# the file that contains the anonymous namespace. By default anonymous namespace
+# are hidden.
+# The default value is: NO.
+
+EXTRACT_ANON_NSPACES   = NO
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all
+# undocumented members inside documented classes or files. If set to NO these
+# members will be included in the various overviews, but no documentation
+# section is generated. This option has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_MEMBERS     = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy. If set
+# to NO, these classes will be included in the various overviews. This option
+# has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_CLASSES     = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend
+# declarations. If set to NO, these declarations will be included in the
+# documentation.
+# The default value is: NO.
+
+HIDE_FRIEND_COMPOUNDS  = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any
+# documentation blocks found inside the body of a function. If set to NO, these
+# blocks will be appended to the function's detailed documentation block.
+# The default value is: NO.
+
+HIDE_IN_BODY_DOCS      = NO
+
+# The INTERNAL_DOCS tag determines if documentation that is typed after a
+# \internal command is included. If the tag is set to NO then the documentation
+# will be excluded. Set it to YES to include the internal documentation.
+# The default value is: NO.
+
+INTERNAL_DOCS          = NO
+
+# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file
+# names in lower-case letters. If set to YES, upper-case letters are also
+# allowed. This is useful if you have classes or files whose names only differ
+# in case and if your file system supports case sensitive file names. Windows
+# (including Cygwin) ands Mac users are advised to set this option to NO.
+# The default value is: system dependent.
+
+CASE_SENSE_NAMES       = YES
+
+# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with
+# their full class and namespace scopes in the documentation. If set to YES, the
+# scope will be hidden.
+# The default value is: NO.
+
+HIDE_SCOPE_NAMES       = NO
+
+# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will
+# append additional text to a page's title, such as Class Reference. If set to
+# YES the compound reference will be hidden.
+# The default value is: NO.
+
+HIDE_COMPOUND_REFERENCE= NO
+
+# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
+# the files that are included by a file in the documentation of that file.
+# The default value is: YES.
+
+SHOW_INCLUDE_FILES     = YES
+
+# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each
+# grouped member an include statement to the documentation, telling the reader
+# which file to include in order to use the member.
+# The default value is: NO.
+
+SHOW_GROUPED_MEMB_INC  = NO
+
+# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include
+# files with double quotes in the documentation rather than with sharp brackets.
+# The default value is: NO.
+
+FORCE_LOCAL_INCLUDES   = NO
+
+# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the
+# documentation for inline members.
+# The default value is: YES.
+
+INLINE_INFO            = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the
+# (detailed) documentation of file and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order.
+# The default value is: YES.
+
+SORT_MEMBER_DOCS       = YES
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
+# descriptions of file, namespace and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order. Note that
+# this will also influence the order of the classes in the class list.
+# The default value is: NO.
+
+SORT_BRIEF_DOCS        = NO
+
+# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the
+# (brief and detailed) documentation of class members so that constructors and
+# destructors are listed first. If set to NO the constructors will appear in the
+# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.
+# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief
+# member documentation.
+# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting
+# detailed member documentation.
+# The default value is: NO.
+
+SORT_MEMBERS_CTORS_1ST = NO
+
+# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy
+# of group names into alphabetical order. If set to NO the group names will
+# appear in their defined order.
+# The default value is: NO.
+
+SORT_GROUP_NAMES       = NO
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
+# fully-qualified names, including namespaces. If set to NO, the class list will
+# be sorted only by class name, not including the namespace part.
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the alphabetical
+# list.
+# The default value is: NO.
+
+SORT_BY_SCOPE_NAME     = NO
+
+# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper
+# type resolution of all parameters of a function it will reject a match between
+# the prototype and the implementation of a member function even if there is
+# only one candidate or it is obvious which candidate to choose by doing a
+# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still
+# accept a match between prototype and implementation in such cases.
+# The default value is: NO.
+
+STRICT_PROTO_MATCHING  = NO
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo
+# list. This list is created by putting \todo commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TODOLIST      = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test
+# list. This list is created by putting \test commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TESTLIST      = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug
+# list. This list is created by putting \bug commands in the documentation.
+# The default value is: YES.
+
+GENERATE_BUGLIST       = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO)
+# the deprecated list. This list is created by putting \deprecated commands in
+# the documentation.
+# The default value is: YES.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional documentation
+# sections, marked by \if <section_label> ... \endif and \cond <section_label>
+# ... \endcond blocks.
+
+ENABLED_SECTIONS       =
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the
+# initial value of a variable or macro / define can have for it to appear in the
+# documentation. If the initializer consists of more lines than specified here
+# it will be hidden. Use a value of 0 to hide initializers completely. The
+# appearance of the value of individual variables and macros / defines can be
+# controlled using \showinitializer or \hideinitializer command in the
+# documentation regardless of this setting.
+# Minimum value: 0, maximum value: 10000, default value: 30.
+
+MAX_INITIALIZER_LINES  = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at
+# the bottom of the documentation of classes and structs. If set to YES, the
+# list will mention the files that were used to generate the documentation.
+# The default value is: YES.
+
+SHOW_USED_FILES        = YES
+
+# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This
+# will remove the Files entry from the Quick Index and from the Folder Tree View
+# (if specified).
+# The default value is: YES.
+
+SHOW_FILES             = YES
+
+# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces
+# page. This will remove the Namespaces entry from the Quick Index and from the
+# Folder Tree View (if specified).
+# The default value is: YES.
+
+SHOW_NAMESPACES        = YES
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that
+# doxygen should invoke to get the current version for each file (typically from
+# the version control system). Doxygen will invoke the program by executing (via
+# popen()) the command command input-file, where command is the value of the
+# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided
+# by doxygen. Whatever the program writes to standard output is used as the file
+# version. For an example see the documentation.
+
+FILE_VERSION_FILTER    =
+
+# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed
+# by doxygen. The layout file controls the global structure of the generated
+# output files in an output format independent way. To create the layout file
+# that represents doxygen's defaults, run doxygen with the -l option. You can
+# optionally specify a file name after the option, if omitted DoxygenLayout.xml
+# will be used as the name of the layout file.
+#
+# Note that if you run doxygen from a directory containing a file called
+# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
+# tag is left empty.
+
+LAYOUT_FILE            =
+
+# The CITE_BIB_FILES tag can be used to specify one or more bib files containing
+# the reference definitions. This must be a list of .bib files. The .bib
+# extension is automatically appended if omitted. This requires the bibtex tool
+# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info.
+# For LaTeX the style of the bibliography can be controlled using
+# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the
+# search path. See also \cite for info how to create references.
+
+CITE_BIB_FILES         =
+
+#---------------------------------------------------------------------------
+# Configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated to
+# standard output by doxygen. If QUIET is set to YES this implies that the
+# messages are off.
+# The default value is: NO.
+
+QUIET                  = NO
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are
+# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES
+# this implies that the warnings are on.
+#
+# Tip: Turn warnings on while writing the documentation.
+# The default value is: YES.
+
+WARNINGS               = YES
+
+# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate
+# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag
+# will automatically be disabled.
+# The default value is: YES.
+
+WARN_IF_UNDOCUMENTED   = YES
+
+# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
+# potential errors in the documentation, such as not documenting some parameters
+# in a documented function, or documenting parameters that don't exist or using
+# markup commands wrongly.
+# The default value is: YES.
+
+WARN_IF_DOC_ERROR      = YES
+
+# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that
+# are documented, but have no documentation for their parameters or return
+# value. If set to NO, doxygen will only warn about wrong or incomplete
+# parameter documentation, but not about the absence of documentation. If
+# EXTRACT_ALL is set to YES then this flag will automatically be disabled.
+# The default value is: NO.
+
+WARN_NO_PARAMDOC       = NO
+
+# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when
+# a warning is encountered.
+# The default value is: NO.
+
+WARN_AS_ERROR          = NO
+
+# The WARN_FORMAT tag determines the format of the warning messages that doxygen
+# can produce. The string should contain the $file, $line, and $text tags, which
+# will be replaced by the file and line number from which the warning originated
+# and the warning text. Optionally the format may contain $version, which will
+# be replaced by the version of the file (if it could be obtained via
+# FILE_VERSION_FILTER)
+# The default value is: $file:$line: $text.
+
+WARN_FORMAT            = "$file:$line: $text"
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning and error
+# messages should be written. If left blank the output is written to standard
+# error (stderr).
+
+WARN_LOGFILE           =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag is used to specify the files and/or directories that contain
+# documented source files. You may enter file names like myfile.cpp or
+# directories like /usr/src/myproject. Separate the files or directories with
+# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
+# Note: If this tag is empty the current directory is searched.
+
+INPUT                  = b_asic
+
+# This tag can be used to specify the character encoding of the source files
+# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
+# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
+# documentation (see: https://www.gnu.org/software/libiconv/) for the list of
+# possible encodings.
+# The default value is: UTF-8.
+
+INPUT_ENCODING         = UTF-8
+
+# If the value of the INPUT tag contains directories, you can use the
+# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
+# *.h) to filter out the source-files in the directories.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# read by doxygen.
+#
+# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
+# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
+# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc,
+# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment),
+# *.doc (to be provided as doxygen C comment), *.txt (to be provided as doxygen
+# C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd,
+# *.vhdl, *.ucf, *.qsf and *.ice.
+
+FILE_PATTERNS          = *.c \
+                         *.cc \
+                         *.cxx \
+                         *.cpp \
+                         *.c++ \
+                         *.java \
+                         *.ii \
+                         *.ixx \
+                         *.ipp \
+                         *.i++ \
+                         *.inl \
+                         *.idl \
+                         *.ddl \
+                         *.odl \
+                         *.h \
+                         *.hh \
+                         *.hxx \
+                         *.hpp \
+                         *.h++ \
+                         *.cs \
+                         *.d \
+                         *.php \
+                         *.php4 \
+                         *.php5 \
+                         *.phtml \
+                         *.inc \
+                         *.m \
+                         *.markdown \
+                         *.md \
+                         *.mm \
+                         *.dox \
+                         *.doc \
+                         *.txt \
+                         *.py \
+                         *.pyw \
+                         *.f90 \
+                         *.f95 \
+                         *.f03 \
+                         *.f08 \
+                         *.f18 \
+                         *.f \
+                         *.for \
+                         *.vhd \
+                         *.vhdl \
+                         *.ucf \
+                         *.qsf \
+                         *.ice
+
+# The RECURSIVE tag can be used to specify whether or not subdirectories should
+# be searched for input files as well.
+# The default value is: NO.
+
+RECURSIVE              = NO
+
+# The EXCLUDE tag can be used to specify files and/or directories that should be
+# excluded from the INPUT source files. This way you can easily exclude a
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+#
+# Note that relative paths are relative to the directory from which doxygen is
+# run.
+
+EXCLUDE                =
+
+# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
+# directories that are symbolic links (a Unix file system feature) are excluded
+# from the input.
+# The default value is: NO.
+
+EXCLUDE_SYMLINKS       = NO
+
+# If the value of the INPUT tag contains directories, you can use the
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
+# certain files from those directories.
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories for example use the pattern */test/*
+
+EXCLUDE_PATTERNS       =
+
+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
+# (namespaces, classes, functions, etc.) that should be excluded from the
+# output. The symbol name can be a fully qualified name, a word, or if the
+# wildcard * is used, a substring. Examples: ANamespace, AClass,
+# AClass::ANamespace, ANamespace::*Test
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories use the pattern */test/*
+
+EXCLUDE_SYMBOLS        =
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or directories
+# that contain example code fragments that are included (see the \include
+# command).
+
+EXAMPLE_PATH           =
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and
+# *.h) to filter out the source-files in the directories. If left blank all
+# files are included.
+
+EXAMPLE_PATTERNS       = *
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
+# searched for input files to be used with the \include or \dontinclude commands
+# irrespective of the value of the RECURSIVE tag.
+# The default value is: NO.
+
+EXAMPLE_RECURSIVE      = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or directories
+# that contain images that are to be included in the documentation (see the
+# \image command).
+
+IMAGE_PATH             = .
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should
+# invoke to filter for each input file. Doxygen will invoke the filter program
+# by executing (via popen()) the command:
+#
+# <filter> <input-file>
+#
+# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the
+# name of an input file. Doxygen will then use the output that the filter
+# program writes to standard output. If FILTER_PATTERNS is specified, this tag
+# will be ignored.
+#
+# Note that the filter must not add or remove lines; it is applied before the
+# code is scanned, but not when the output code is generated. If lines are added
+# or removed, the anchors will not be placed correctly.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+INPUT_FILTER           =
+
+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
+# basis. Doxygen will compare the file name with each pattern and apply the
+# filter if there is a match. The filters are a list of the form: pattern=filter
+# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how
+# filters are used. If the FILTER_PATTERNS tag is empty or if none of the
+# patterns match the file name, INPUT_FILTER is applied.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+FILTER_PATTERNS        =
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
+# INPUT_FILTER) will also be used to filter the input files that are used for
+# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).
+# The default value is: NO.
+
+FILTER_SOURCE_FILES    = NO
+
+# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file
+# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and
+# it is also possible to disable source filtering for a specific pattern using
+# *.ext= (so without naming a filter).
+# This tag requires that the tag FILTER_SOURCE_FILES is set to YES.
+
+FILTER_SOURCE_PATTERNS =
+
+# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that
+# is part of the input, its contents will be placed on the main page
+# (index.html). This can be useful if you have a project on for instance GitHub
+# and want to reuse the introduction page also for the doxygen output.
+
+USE_MDFILE_AS_MAINPAGE =
+
+#---------------------------------------------------------------------------
+# Configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will be
+# generated. Documented entities will be cross-referenced with these sources.
+#
+# Note: To get rid of all source code in the generated output, make sure that
+# also VERBATIM_HEADERS is set to NO.
+# The default value is: NO.
+
+SOURCE_BROWSER         = NO
+
+# Setting the INLINE_SOURCES tag to YES will include the body of functions,
+# classes and enums directly into the documentation.
+# The default value is: NO.
+
+INLINE_SOURCES         = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any
+# special comment blocks from generated source code fragments. Normal C, C++ and
+# Fortran comments will always remain visible.
+# The default value is: YES.
+
+STRIP_CODE_COMMENTS    = YES
+
+# If the REFERENCED_BY_RELATION tag is set to YES then for each documented
+# entity all documented functions referencing it will be listed.
+# The default value is: NO.
+
+REFERENCED_BY_RELATION = NO
+
+# If the REFERENCES_RELATION tag is set to YES then for each documented function
+# all documented entities called/used by that function will be listed.
+# The default value is: NO.
+
+REFERENCES_RELATION    = NO
+
+# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set
+# to YES then the hyperlinks from functions in REFERENCES_RELATION and
+# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will
+# link to the documentation.
+# The default value is: YES.
+
+REFERENCES_LINK_SOURCE = YES
+
+# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the
+# source code will show a tooltip with additional information such as prototype,
+# brief description and links to the definition and documentation. Since this
+# will make the HTML file larger and loading of large files a bit slower, you
+# can opt to disable this feature.
+# The default value is: YES.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+SOURCE_TOOLTIPS        = YES
+
+# If the USE_HTAGS tag is set to YES then the references to source code will
+# point to the HTML generated by the htags(1) tool instead of doxygen built-in
+# source browser. The htags tool is part of GNU's global source tagging system
+# (see https://www.gnu.org/software/global/global.html). You will need version
+# 4.8.6 or higher.
+#
+# To use it do the following:
+# - Install the latest version of global
+# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file
+# - Make sure the INPUT points to the root of the source tree
+# - Run doxygen as normal
+#
+# Doxygen will invoke htags (and that will in turn invoke gtags), so these
+# tools must be available from the command line (i.e. in the search path).
+#
+# The result: instead of the source browser generated by doxygen, the links to
+# source code will now point to the output of htags.
+# The default value is: NO.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+USE_HTAGS              = NO
+
+# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a
+# verbatim copy of the header file for each class for which an include is
+# specified. Set to NO to disable this.
+# See also: Section \class.
+# The default value is: YES.
+
+VERBATIM_HEADERS       = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all
+# compounds will be generated. Enable this if the project contains a lot of
+# classes, structs, unions or interfaces.
+# The default value is: YES.
+
+ALPHABETICAL_INDEX     = YES
+
+# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in
+# which the alphabetical index list will be split.
+# Minimum value: 1, maximum value: 20, default value: 5.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+COLS_IN_ALPHA_INDEX    = 5
+
+# In case all classes in a project start with a common prefix, all classes will
+# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag
+# can be used to specify a prefix (or a list of prefixes) that should be ignored
+# while generating the index headers.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+IGNORE_PREFIX          =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
+# The default value is: YES.
+
+GENERATE_HTML          = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_OUTPUT            = html
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each
+# generated HTML page (for example: .htm, .php, .asp).
+# The default value is: .html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FILE_EXTENSION    = .html
+
+# The HTML_HEADER tag can be used to specify a user-defined HTML header file for
+# each generated HTML page. If the tag is left blank doxygen will generate a
+# standard header.
+#
+# To get valid HTML the header file that includes any scripts and style sheets
+# that doxygen needs, which is dependent on the configuration options used (e.g.
+# the setting GENERATE_TREEVIEW). It is highly recommended to start with a
+# default header using
+# doxygen -w html new_header.html new_footer.html new_stylesheet.css
+# YourConfigFile
+# and then modify the file new_header.html. See also section "Doxygen usage"
+# for information on how to generate the default header that doxygen normally
+# uses.
+# Note: The header is subject to change so you typically have to regenerate the
+# default header when upgrading to a newer version of doxygen. For a description
+# of the possible markers and block names see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_HEADER            =
+
+# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each
+# generated HTML page. If the tag is left blank doxygen will generate a standard
+# footer. See HTML_HEADER for more information on how to generate a default
+# footer and what special commands can be used inside the footer. See also
+# section "Doxygen usage" for information on how to generate the default footer
+# that doxygen normally uses.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FOOTER            =
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style
+# sheet that is used by each HTML page. It can be used to fine-tune the look of
+# the HTML output. If left blank doxygen will generate a default style sheet.
+# See also section "Doxygen usage" for information on how to generate the style
+# sheet that doxygen normally uses.
+# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as
+# it is more robust and this tag (HTML_STYLESHEET) will in the future become
+# obsolete.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_STYLESHEET        =
+
+# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# cascading style sheets that are included after the standard style sheets
+# created by doxygen. Using this option one can overrule certain style aspects.
+# This is preferred over using HTML_STYLESHEET since it does not replace the
+# standard style sheet and is therefore more robust against future updates.
+# Doxygen will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list). For an example see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_STYLESHEET  =
+
+# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the HTML output directory. Note
+# that these files will be copied to the base HTML output directory. Use the
+# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these
+# files. In the HTML_STYLESHEET file, use the file name only. Also note that the
+# files will be copied as-is; there are no commands or markers available.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_FILES       =
+
+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
+# will adjust the colors in the style sheet and background images according to
+# this color. Hue is specified as an angle on a colorwheel, see
+# https://en.wikipedia.org/wiki/Hue for more information. For instance the value
+# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
+# purple, and 360 is red again.
+# Minimum value: 0, maximum value: 359, default value: 220.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_HUE    = 220
+
+# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
+# in the HTML output. For a value of 0 the output will use grayscales only. A
+# value of 255 will produce the most vivid colors.
+# Minimum value: 0, maximum value: 255, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_SAT    = 100
+
+# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the
+# luminance component of the colors in the HTML output. Values below 100
+# gradually make the output lighter, whereas values above 100 make the output
+# darker. The value divided by 100 is the actual gamma applied, so 80 represents
+# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not
+# change the gamma.
+# Minimum value: 40, maximum value: 240, default value: 80.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_GAMMA  = 80
+
+# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
+# page will contain the date and time when the page was generated. Setting this
+# to YES can help to show when doxygen was last run and thus if the
+# documentation is up to date.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_TIMESTAMP         = NO
+
+# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
+# documentation will contain a main index with vertical navigation menus that
+# are dynamically created via JavaScript. If disabled, the navigation index will
+# consists of multiple levels of tabs that are statically embedded in every HTML
+# page. Disable this option to support browsers that do not have JavaScript,
+# like the Qt help browser.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_MENUS     = YES
+
+# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
+# documentation will contain sections that can be hidden and shown after the
+# page has loaded.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_SECTIONS  = NO
+
+# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
+# shown in the various tree structured indices initially; the user can expand
+# and collapse entries dynamically later on. Doxygen will expand the tree to
+# such a level that at most the specified number of entries are visible (unless
+# a fully collapsed tree already exceeds this amount). So setting the number of
+# entries 1 will produce a full collapsed tree by default. 0 is a special value
+# representing an infinite number of entries and will result in a full expanded
+# tree by default.
+# Minimum value: 0, maximum value: 9999, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_INDEX_NUM_ENTRIES = 100
+
+# If the GENERATE_DOCSET tag is set to YES, additional index files will be
+# generated that can be used as input for Apple's Xcode 3 integrated development
+# environment (see: https://developer.apple.com/xcode/), introduced with OSX
+# 10.5 (Leopard). To create a documentation set, doxygen will generate a
+# Makefile in the HTML output directory. Running make will produce the docset in
+# that directory and running make install will install the docset in
+# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at
+# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy
+# genXcode/_index.html for more information.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_DOCSET        = NO
+
+# This tag determines the name of the docset feed. A documentation feed provides
+# an umbrella under which multiple documentation sets from a single provider
+# (such as a company or product suite) can be grouped.
+# The default value is: Doxygen generated docs.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_FEEDNAME        = "Doxygen generated docs"
+
+# This tag specifies a string that should uniquely identify the documentation
+# set bundle. This should be a reverse domain-name style string, e.g.
+# com.mycompany.MyDocSet. Doxygen will append .docset to the name.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_BUNDLE_ID       = org.doxygen.Project
+
+# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify
+# the documentation publisher. This should be a reverse domain-name style
+# string, e.g. com.mycompany.MyDocSet.documentation.
+# The default value is: org.doxygen.Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_ID    = org.doxygen.Publisher
+
+# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.
+# The default value is: Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_NAME  = Publisher
+
+# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
+# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
+# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
+# (see: https://www.microsoft.com/en-us/download/details.aspx?id=21138) on
+# Windows.
+#
+# The HTML Help Workshop contains a compiler that can convert all HTML output
+# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
+# files are now used as the Windows 98 help format, and will replace the old
+# Windows help format (.hlp) on all Windows platforms in the future. Compressed
+# HTML files also contain an index, a table of contents, and you can search for
+# words in the documentation. The HTML workshop also contains a viewer for
+# compressed HTML files.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_HTMLHELP      = NO
+
+# The CHM_FILE tag can be used to specify the file name of the resulting .chm
+# file. You can add a path in front of the file if the result should not be
+# written to the html output directory.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_FILE               =
+
+# The HHC_LOCATION tag can be used to specify the location (absolute path
+# including file name) of the HTML help compiler (hhc.exe). If non-empty,
+# doxygen will try to run the HTML help compiler on the generated index.hhp.
+# The file has to be specified with full path.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+HHC_LOCATION           =
+
+# The GENERATE_CHI flag controls if a separate .chi index file is generated
+# (YES) or that it should be included in the master .chm file (NO).
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+GENERATE_CHI           = NO
+
+# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)
+# and project file content.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_INDEX_ENCODING     =
+
+# The BINARY_TOC flag controls whether a binary table of contents is generated
+# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it
+# enables the Previous and Next buttons.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+BINARY_TOC             = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members to
+# the table of contents of the HTML help documentation and to the tree view.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+TOC_EXPAND             = NO
+
+# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
+# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
+# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
+# (.qch) of the generated HTML documentation.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_QHP           = NO
+
+# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify
+# the file name of the resulting .qch file. The path specified is relative to
+# the HTML output folder.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QCH_FILE               =
+
+# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help
+# Project output. For more information please see Qt Help Project / Namespace
+# (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace).
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_NAMESPACE          = org.doxygen.Project
+
+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt
+# Help Project output. For more information please see Qt Help Project / Virtual
+# Folders (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-
+# folders).
+# The default value is: doc.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_VIRTUAL_FOLDER     = doc
+
+# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom
+# filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_NAME   =
+
+# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the
+# custom filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_ATTRS  =
+
+# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
+# project's filter section matches. Qt Help Project / Filter Attributes (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_SECT_FILTER_ATTRS  =
+
+# The QHG_LOCATION tag can be used to specify the location of Qt's
+# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the
+# generated .qhp file.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHG_LOCATION           =
+
+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be
+# generated, together with the HTML files, they form an Eclipse help plugin. To
+# install this plugin and make it available under the help contents menu in
+# Eclipse, the contents of the directory containing the HTML and XML files needs
+# to be copied into the plugins directory of eclipse. The name of the directory
+# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.
+# After copying Eclipse needs to be restarted before the help appears.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_ECLIPSEHELP   = NO
+
+# A unique identifier for the Eclipse help plugin. When installing the plugin
+# the directory name containing the HTML and XML files should also have this
+# name. Each documentation set should have its own identifier.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.
+
+ECLIPSE_DOC_ID         = org.doxygen.Project
+
+# If you want full control over the layout of the generated HTML pages it might
+# be necessary to disable the index and replace it with your own. The
+# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top
+# of each HTML page. A value of NO enables the index and the value YES disables
+# it. Since the tabs in the index contain the same information as the navigation
+# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+DISABLE_INDEX          = NO
+
+# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
+# structure should be generated to display hierarchical information. If the tag
+# value is set to YES, a side panel will be generated containing a tree-like
+# index structure (just like the one that is generated for HTML Help). For this
+# to work a browser that supports JavaScript, DHTML, CSS and frames is required
+# (i.e. any modern browser). Windows users are probably better off using the
+# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
+# further fine-tune the look of the index. As an example, the default style
+# sheet generated by doxygen has an example that shows how to put an image at
+# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
+# the same information as the tab index, you could consider setting
+# DISABLE_INDEX to YES when enabling this option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_TREEVIEW      = NO
+
+# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
+# doxygen will group on one line in the generated HTML documentation.
+#
+# Note that a value of 0 will completely suppress the enum values from appearing
+# in the overview section.
+# Minimum value: 0, maximum value: 20, default value: 4.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+ENUM_VALUES_PER_LINE   = 4
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used
+# to set the initial width (in pixels) of the frame in which the tree is shown.
+# Minimum value: 0, maximum value: 1500, default value: 250.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+TREEVIEW_WIDTH         = 250
+
+# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to
+# external symbols imported via tag files in a separate window.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+EXT_LINKS_IN_WINDOW    = NO
+
+# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg
+# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see
+# https://inkscape.org) to generate formulas as SVG images instead of PNGs for
+# the HTML output. These images will generally look nicer at scaled resolutions.
+# Possible values are: png The default and svg Looks nicer but requires the
+# pdf2svg tool.
+# The default value is: png.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FORMULA_FORMAT    = png
+
+# Use this tag to change the font size of LaTeX formulas included as images in
+# the HTML documentation. When you change the font size after a successful
+# doxygen run you need to manually remove any form_*.png images from the HTML
+# output directory to force them to be regenerated.
+# Minimum value: 8, maximum value: 50, default value: 10.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_FONTSIZE       = 10
+
+# Use the FORMULA_TRANSPARENT tag to determine whether or not the images
+# generated for formulas are transparent PNGs. Transparent PNGs are not
+# supported properly for IE 6.0, but are supported on all modern browsers.
+#
+# Note that when changing this option you need to delete any form_*.png files in
+# the HTML output directory before the changes have effect.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_TRANSPARENT    = YES
+
+# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands
+# to create new LaTeX commands to be used in formulas as building blocks. See
+# the section "Including formulas" for details.
+
+FORMULA_MACROFILE      =
+
+# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see
+# https://www.mathjax.org) which uses client side JavaScript for the rendering
+# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX
+# installed or if you want to formulas look prettier in the HTML output. When
+# enabled you may also need to install MathJax separately and configure the path
+# to it using the MATHJAX_RELPATH option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+USE_MATHJAX            = NO
+
+# When MathJax is enabled you can set the default output format to be used for
+# the MathJax output. See the MathJax site (see:
+# http://docs.mathjax.org/en/latest/output.html) for more details.
+# Possible values are: HTML-CSS (which is slower, but has the best
+# compatibility), NativeMML (i.e. MathML) and SVG.
+# The default value is: HTML-CSS.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_FORMAT         = HTML-CSS
+
+# When MathJax is enabled you need to specify the location relative to the HTML
+# output directory using the MATHJAX_RELPATH option. The destination directory
+# should contain the MathJax.js script. For instance, if the mathjax directory
+# is located at the same level as the HTML output directory, then
+# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
+# Content Delivery Network so you can quickly see the result without installing
+# MathJax. However, it is strongly recommended to install a local copy of
+# MathJax from https://www.mathjax.org before deployment.
+# The default value is: https://cdn.jsdelivr.net/npm/mathjax@2.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_RELPATH        = https://cdn.jsdelivr.net/npm/mathjax@2
+
+# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
+# extension names that should be enabled during MathJax rendering. For example
+# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_EXTENSIONS     =
+
+# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces
+# of code that will be used on startup of the MathJax code. See the MathJax site
+# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an
+# example see the documentation.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_CODEFILE       =
+
+# When the SEARCHENGINE tag is enabled doxygen will generate a search box for
+# the HTML output. The underlying search engine uses javascript and DHTML and
+# should work on any modern browser. Note that when using HTML help
+# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)
+# there is already a search function so this one should typically be disabled.
+# For large projects the javascript based search engine can be slow, then
+# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to
+# search using the keyboard; to jump to the search box use <access key> + S
+# (what the <access key> is depends on the OS and browser, but it is typically
+# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down
+# key> to jump into the search results window, the results can be navigated
+# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel
+# the search. The filter options can be selected when the cursor is inside the
+# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys>
+# to select a filter and <Enter> or <escape> to activate or cancel the filter
+# option.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+SEARCHENGINE           = YES
+
+# When the SERVER_BASED_SEARCH tag is enabled the search engine will be
+# implemented using a web server instead of a web client using JavaScript. There
+# are two flavors of web server based searching depending on the EXTERNAL_SEARCH
+# setting. When disabled, doxygen will generate a PHP script for searching and
+# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing
+# and searching needs to be provided by external tools. See the section
+# "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SERVER_BASED_SEARCH    = NO
+
+# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP
+# script for searching. Instead the search results are written to an XML file
+# which needs to be processed by an external indexer. Doxygen will invoke an
+# external search engine pointed to by the SEARCHENGINE_URL option to obtain the
+# search results.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: https://xapian.org/).
+#
+# See the section "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH        = NO
+
+# The SEARCHENGINE_URL should point to a search engine hosted by a web server
+# which will return the search results when EXTERNAL_SEARCH is enabled.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: https://xapian.org/). See the section "External Indexing and
+# Searching" for details.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHENGINE_URL       =
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed
+# search data is written to a file for indexing by an external tool. With the
+# SEARCHDATA_FILE tag the name of this file can be specified.
+# The default file is: searchdata.xml.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHDATA_FILE        = searchdata.xml
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the
+# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is
+# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple
+# projects and redirect the results back to the right project.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH_ID     =
+
+# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen
+# projects other than the one defined by this configuration file, but that are
+# all added to the same external search index. Each project needs to have a
+# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of
+# to a relative location where the documentation can be found. The format is:
+# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTRA_SEARCH_MAPPINGS  =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.
+# The default value is: YES.
+
+GENERATE_LATEX         = YES
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_OUTPUT           = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be
+# invoked.
+#
+# Note that when not enabling USE_PDFLATEX the default is latex when enabling
+# USE_PDFLATEX the default is pdflatex and when in the later case latex is
+# chosen this is overwritten by pdflatex. For specific output languages the
+# default can have been set differently, this depends on the implementation of
+# the output language.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_CMD_NAME         =
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate
+# index for LaTeX.
+# Note: This tag is used in the Makefile / make.bat.
+# See also: LATEX_MAKEINDEX_CMD for the part in the generated output file
+# (.tex).
+# The default file is: makeindex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+MAKEINDEX_CMD_NAME     = makeindex
+
+# The LATEX_MAKEINDEX_CMD tag can be used to specify the command name to
+# generate index for LaTeX. In case there is no backslash (\) as first character
+# it will be automatically added in the LaTeX code.
+# Note: This tag is used in the generated output file (.tex).
+# See also: MAKEINDEX_CMD_NAME for the part in the Makefile / make.bat.
+# The default value is: makeindex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_MAKEINDEX_CMD    = makeindex
+
+# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+COMPACT_LATEX          = NO
+
+# The PAPER_TYPE tag can be used to set the paper type that is used by the
+# printer.
+# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x
+# 14 inches) and executive (7.25 x 10.5 inches).
+# The default value is: a4.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PAPER_TYPE             = a4
+
+# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names
+# that should be included in the LaTeX output. The package can be specified just
+# by its name or with the correct syntax as to be used with the LaTeX
+# \usepackage command. To get the times font for instance you can specify :
+# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times}
+# To use the option intlimits with the amsmath package you can specify:
+# EXTRA_PACKAGES=[intlimits]{amsmath}
+# If left blank no extra packages will be included.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+EXTRA_PACKAGES         =
+
+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the
+# generated LaTeX document. The header should contain everything until the first
+# chapter. If it is left blank doxygen will generate a standard header. See
+# section "Doxygen usage" for information on how to let doxygen write the
+# default header to a separate file.
+#
+# Note: Only use a user-defined header if you know what you are doing! The
+# following commands have a special meaning inside the header: $title,
+# $datetime, $date, $doxygenversion, $projectname, $projectnumber,
+# $projectbrief, $projectlogo. Doxygen will replace $title with the empty
+# string, for the replacement values of the other commands the user is referred
+# to HTML_HEADER.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HEADER           =
+
+# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the
+# generated LaTeX document. The footer should contain everything after the last
+# chapter. If it is left blank doxygen will generate a standard footer. See
+# LATEX_HEADER for more information on how to generate a default footer and what
+# special commands can be used inside the footer.
+#
+# Note: Only use a user-defined footer if you know what you are doing!
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_FOOTER           =
+
+# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# LaTeX style sheets that are included after the standard style sheets created
+# by doxygen. Using this option one can overrule certain style aspects. Doxygen
+# will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list).
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_STYLESHEET =
+
+# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the LATEX_OUTPUT output
+# directory. Note that the files will be copied as-is; there are no commands or
+# markers available.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_FILES      =
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is
+# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will
+# contain links (just like the HTML output) instead of page references. This
+# makes the output suitable for online browsing using a PDF viewer.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PDF_HYPERLINKS         = YES
+
+# If the USE_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate
+# the PDF file directly from the LaTeX files. Set this option to YES, to get a
+# higher quality PDF documentation.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+USE_PDFLATEX           = YES
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
+# command to the generated LaTeX files. This will instruct LaTeX to keep running
+# if errors occur, instead of asking the user for help. This option is also used
+# when generating formulas in HTML.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BATCHMODE        = NO
+
+# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the
+# index chapters (such as File Index, Compound Index, etc.) in the output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HIDE_INDICES     = NO
+
+# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source
+# code with syntax highlighting in the LaTeX output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_SOURCE_CODE      = NO
+
+# The LATEX_BIB_STYLE tag can be used to specify the style to use for the
+# bibliography, e.g. plainnat, or ieeetr. See
+# https://en.wikipedia.org/wiki/BibTeX and \cite for more info.
+# The default value is: plain.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BIB_STYLE        = plain
+
+# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
+# page will contain the date and time when the page was generated. Setting this
+# to NO can help when comparing the output of multiple runs.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_TIMESTAMP        = NO
+
+# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)
+# path from which the emoji images will be read. If a relative path is entered,
+# it will be relative to the LATEX_OUTPUT directory. If left blank the
+# LATEX_OUTPUT directory will be used.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EMOJI_DIRECTORY  =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The
+# RTF output is optimized for Word 97 and may not look too pretty with other RTF
+# readers/editors.
+# The default value is: NO.
+
+GENERATE_RTF           = NO
+
+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: rtf.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_OUTPUT             = rtf
+
+# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+COMPACT_RTF            = NO
+
+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will
+# contain hyperlink fields. The RTF file will contain links (just like the HTML
+# output) instead of page references. This makes the output suitable for online
+# browsing using Word or some other Word compatible readers that support those
+# fields.
+#
+# Note: WordPad (write) and others do not support links.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_HYPERLINKS         = NO
+
+# Load stylesheet definitions from file. Syntax is similar to doxygen's
+# configuration file, i.e. a series of assignments. You only have to provide
+# replacements, missing definitions are set to their default value.
+#
+# See also section "Doxygen usage" for information on how to generate the
+# default style sheet that doxygen normally uses.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_STYLESHEET_FILE    =
+
+# Set optional variables used in the generation of an RTF document. Syntax is
+# similar to doxygen's configuration file. A template extensions file can be
+# generated using doxygen -e rtf extensionFile.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_EXTENSIONS_FILE    =
+
+# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code
+# with syntax highlighting in the RTF output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_SOURCE_CODE        = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for
+# classes and files.
+# The default value is: NO.
+
+GENERATE_MAN           = NO
+
+# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it. A directory man3 will be created inside the directory specified by
+# MAN_OUTPUT.
+# The default directory is: man.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_OUTPUT             = man
+
+# The MAN_EXTENSION tag determines the extension that is added to the generated
+# man pages. In case the manual section does not start with a number, the number
+# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is
+# optional.
+# The default value is: .3.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_EXTENSION          = .3
+
+# The MAN_SUBDIR tag determines the name of the directory created within
+# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by
+# MAN_EXTENSION with the initial . removed.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_SUBDIR             =
+
+# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it
+# will generate one additional man file for each entity documented in the real
+# man page(s). These additional files only source the real man page, but without
+# them the man command would be unable to find the correct page.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_LINKS              = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that
+# captures the structure of the code including all documentation.
+# The default value is: NO.
+
+GENERATE_XML           = NO
+
+# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: xml.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_OUTPUT             = xml
+
+# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program
+# listings (including syntax highlighting and cross-referencing information) to
+# the XML output. Note that enabling this will significantly increase the size
+# of the XML output.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_PROGRAMLISTING     = YES
+
+# If the XML_NS_MEMB_FILE_SCOPE tag is set to YES, doxygen will include
+# namespace members in file scope as well, matching the HTML output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_NS_MEMB_FILE_SCOPE = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the DOCBOOK output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files
+# that can be used to generate PDF.
+# The default value is: NO.
+
+GENERATE_DOCBOOK       = NO
+
+# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in
+# front of it.
+# The default directory is: docbook.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_OUTPUT         = docbook
+
+# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the
+# program listings (including syntax highlighting and cross-referencing
+# information) to the DOCBOOK output. Note that enabling this will significantly
+# increase the size of the DOCBOOK output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_PROGRAMLISTING = NO
+
+#---------------------------------------------------------------------------
+# Configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
+# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures
+# the structure of the code including all documentation. Note that this feature
+# is still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_AUTOGEN_DEF   = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module
+# file that captures the structure of the code including all documentation.
+#
+# Note that this feature is still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_PERLMOD       = NO
+
+# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary
+# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI
+# output from the Perl module output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_LATEX          = NO
+
+# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely
+# formatted so it can be parsed by a human reader. This is useful if you want to
+# understand what is going on. On the other hand, if this tag is set to NO, the
+# size of the Perl module output will be much smaller and Perl will parse it
+# just the same.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_PRETTY         = YES
+
+# The names of the make variables in the generated doxyrules.make file are
+# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful
+# so different doxyrules.make files included by the same Makefile don't
+# overwrite each other's variables.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_MAKEVAR_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all
+# C-preprocessor directives found in the sources and include files.
+# The default value is: YES.
+
+ENABLE_PREPROCESSING   = YES
+
+# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names
+# in the source code. If set to NO, only conditional compilation will be
+# performed. Macro expansion can be done in a controlled way by setting
+# EXPAND_ONLY_PREDEF to YES.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+MACRO_EXPANSION        = NO
+
+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then
+# the macro expansion is limited to the macros specified with the PREDEFINED and
+# EXPAND_AS_DEFINED tags.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_ONLY_PREDEF     = NO
+
+# If the SEARCH_INCLUDES tag is set to YES, the include files in the
+# INCLUDE_PATH will be searched if a #include is found.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SEARCH_INCLUDES        = YES
+
+# The INCLUDE_PATH tag can be used to specify one or more directories that
+# contain include files that are not input files but should be processed by the
+# preprocessor.
+# This tag requires that the tag SEARCH_INCLUDES is set to YES.
+
+INCLUDE_PATH           =
+
+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
+# patterns (like *.h and *.hpp) to filter out the header-files in the
+# directories. If left blank, the patterns specified with FILE_PATTERNS will be
+# used.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+INCLUDE_FILE_PATTERNS  =
+
+# The PREDEFINED tag can be used to specify one or more macro names that are
+# defined before the preprocessor is started (similar to the -D option of e.g.
+# gcc). The argument of the tag is a list of macros of the form: name or
+# name=definition (no spaces). If the definition and the "=" are omitted, "=1"
+# is assumed. To prevent a macro definition from being undefined via #undef or
+# recursively expanded use the := operator instead of the = operator.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+PREDEFINED             =
+
+# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
+# tag can be used to specify a list of macro names that should be expanded. The
+# macro definition that is found in the sources will be used. Use the PREDEFINED
+# tag if you want to use a different macro definition that overrules the
+# definition found in the source code.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_AS_DEFINED      =
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will
+# remove all references to function-like macros that are alone on a line, have
+# an all uppercase name, and do not end with a semicolon. Such function macros
+# are typically used for boiler-plate code, and will confuse the parser if not
+# removed.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SKIP_FUNCTION_MACROS   = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to external references
+#---------------------------------------------------------------------------
+
+# The TAGFILES tag can be used to specify one or more tag files. For each tag
+# file the location of the external documentation should be added. The format of
+# a tag file without this location is as follows:
+# TAGFILES = file1 file2 ...
+# Adding location for the tag files is done as follows:
+# TAGFILES = file1=loc1 "file2 = loc2" ...
+# where loc1 and loc2 can be relative or absolute paths or URLs. See the
+# section "Linking to external documentation" for more information about the use
+# of tag files.
+# Note: Each tag file must have a unique name (where the name does NOT include
+# the path). If a tag file is not located in the directory in which doxygen is
+# run, you must also specify the path to the tagfile here.
+
+TAGFILES               =
+
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create a
+# tag file that is based on the input files it reads. See section "Linking to
+# external documentation" for more information about the usage of tag files.
+
+GENERATE_TAGFILE       =
+
+# If the ALLEXTERNALS tag is set to YES, all external class will be listed in
+# the class index. If set to NO, only the inherited external classes will be
+# listed.
+# The default value is: NO.
+
+ALLEXTERNALS           = NO
+
+# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
+# in the modules index. If set to NO, only the current project's groups will be
+# listed.
+# The default value is: YES.
+
+EXTERNAL_GROUPS        = YES
+
+# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in
+# the related pages index. If set to NO, only the current project's pages will
+# be listed.
+# The default value is: YES.
+
+EXTERNAL_PAGES         = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram
+# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to
+# NO turns the diagrams off. Note that this option also works with HAVE_DOT
+# disabled, but it is recommended to install and use dot, since it yields more
+# powerful graphs.
+# The default value is: YES.
+
+CLASS_DIAGRAMS         = YES
+
+# You can include diagrams made with dia in doxygen documentation. Doxygen will
+# then run dia to produce the diagram and insert it in the documentation. The
+# DIA_PATH tag allows you to specify the directory where the dia binary resides.
+# If left empty dia is assumed to be found in the default search path.
+
+DIA_PATH               =
+
+# If set to YES the inheritance and collaboration graphs will hide inheritance
+# and usage relations if the target is undocumented or is not a class.
+# The default value is: YES.
+
+HIDE_UNDOC_RELATIONS   = YES
+
+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
+# available from the path. This tool is part of Graphviz (see:
+# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
+# Bell Labs. The other options in this section have no effect if this option is
+# set to NO
+# The default value is: NO.
+
+HAVE_DOT               = NO
+
+# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed
+# to run in parallel. When set to 0 doxygen will base this on the number of
+# processors available in the system. You can set it explicitly to a value
+# larger than 0 to get control over the balance between CPU load and processing
+# speed.
+# Minimum value: 0, maximum value: 32, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_NUM_THREADS        = 0
+
+# When you want a differently looking font in the dot files that doxygen
+# generates you can specify the font name using DOT_FONTNAME. You need to make
+# sure dot is able to find the font, which can be done by putting it in a
+# standard location or by setting the DOTFONTPATH environment variable or by
+# setting DOT_FONTPATH to the directory containing the font.
+# The default value is: Helvetica.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTNAME           = Helvetica
+
+# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
+# dot graphs.
+# Minimum value: 4, maximum value: 24, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTSIZE           = 10
+
+# By default doxygen will tell dot to use the default font as specified with
+# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
+# the path where dot can find it using this tag.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTPATH           =
+
+# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for
+# each documented class showing the direct and indirect inheritance relations.
+# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CLASS_GRAPH            = YES
+
+# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
+# graph for each documented class showing the direct and indirect implementation
+# dependencies (inheritance, containment, and class references variables) of the
+# class with other documented classes.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+COLLABORATION_GRAPH    = YES
+
+# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
+# groups, showing the direct groups dependencies.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GROUP_GRAPHS           = YES
+
+# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and
+# collaboration diagrams in a style similar to the OMG's Unified Modeling
+# Language.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LOOK               = NO
+
+# If the UML_LOOK tag is enabled, the fields and methods are shown inside the
+# class node. If there are many fields or methods and many nodes the graph may
+# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the
+# number of items for each type to make the size more manageable. Set this to 0
+# for no limit. Note that the threshold may be exceeded by 50% before the limit
+# is enforced. So when you set the threshold to 10, up to 15 fields may appear,
+# but if the number exceeds 15, the total amount of fields shown is limited to
+# 10.
+# Minimum value: 0, maximum value: 100, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LIMIT_NUM_FIELDS   = 10
+
+# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and
+# collaboration graphs will show the relations between templates and their
+# instances.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+TEMPLATE_RELATIONS     = NO
+
+# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
+# YES then doxygen will generate a graph for each documented file showing the
+# direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDE_GRAPH          = YES
+
+# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
+# set to YES then doxygen will generate a graph for each documented file showing
+# the direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDED_BY_GRAPH      = YES
+
+# If the CALL_GRAPH tag is set to YES then doxygen will generate a call
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable call graphs for selected
+# functions only using the \callgraph command. Disabling a call graph can be
+# accomplished by means of the command \hidecallgraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALL_GRAPH             = NO
+
+# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable caller graphs for selected
+# functions only using the \callergraph command. Disabling a caller graph can be
+# accomplished by means of the command \hidecallergraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALLER_GRAPH           = NO
+
+# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical
+# hierarchy of all classes instead of a textual one.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GRAPHICAL_HIERARCHY    = YES
+
+# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
+# dependencies a directory has on other directories in a graphical way. The
+# dependency relations are determined by the #include relations between the
+# files in the directories.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DIRECTORY_GRAPH        = YES
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
+# generated by dot. For an explanation of the image formats see the section
+# output formats in the documentation of the dot tool (Graphviz (see:
+# http://www.graphviz.org/)).
+# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
+# to make the SVG files visible in IE 9+ (other browsers do not have this
+# requirement).
+# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo,
+# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
+# png:gdiplus:gdiplus.
+# The default value is: png.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_IMAGE_FORMAT       = png
+
+# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to
+# enable generation of interactive SVG images that allow zooming and panning.
+#
+# Note that this requires a modern browser other than Internet Explorer. Tested
+# and working are Firefox, Chrome, Safari, and Opera.
+# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make
+# the SVG files visible. Older versions of IE do not have SVG support.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INTERACTIVE_SVG        = NO
+
+# The DOT_PATH tag can be used to specify the path where the dot tool can be
+# found. If left blank, it is assumed the dot tool can be found in the path.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_PATH               =
+
+# The DOTFILE_DIRS tag can be used to specify one or more directories that
+# contain dot files that are included in the documentation (see the \dotfile
+# command).
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOTFILE_DIRS           =
+
+# The MSCFILE_DIRS tag can be used to specify one or more directories that
+# contain msc files that are included in the documentation (see the \mscfile
+# command).
+
+MSCFILE_DIRS           =
+
+# The DIAFILE_DIRS tag can be used to specify one or more directories that
+# contain dia files that are included in the documentation (see the \diafile
+# command).
+
+DIAFILE_DIRS           =
+
+# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the
+# path where java can find the plantuml.jar file. If left blank, it is assumed
+# PlantUML is not used or called during a preprocessing step. Doxygen will
+# generate a warning when it encounters a \startuml command in this case and
+# will not generate output for the diagram.
+
+PLANTUML_JAR_PATH      =
+
+# When using plantuml, the PLANTUML_CFG_FILE tag can be used to specify a
+# configuration file for plantuml.
+
+PLANTUML_CFG_FILE      =
+
+# When using plantuml, the specified paths are searched for files specified by
+# the !include statement in a plantuml block.
+
+PLANTUML_INCLUDE_PATH  =
+
+# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes
+# that will be shown in the graph. If the number of nodes in a graph becomes
+# larger than this value, doxygen will truncate the graph, which is visualized
+# by representing a node as a red box. Note that doxygen if the number of direct
+# children of the root node in a graph is already larger than
+# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that
+# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.
+# Minimum value: 0, maximum value: 10000, default value: 50.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_GRAPH_MAX_NODES    = 50
+
+# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs
+# generated by dot. A depth value of 3 means that only nodes reachable from the
+# root by following a path via at most 3 edges will be shown. Nodes that lay
+# further from the root node will be omitted. Note that setting this option to 1
+# or 2 may greatly reduce the computation time needed for large code bases. Also
+# note that the size of a graph can be further restricted by
+# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.
+# Minimum value: 0, maximum value: 1000, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+MAX_DOT_GRAPH_DEPTH    = 0
+
+# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
+# background. This is disabled by default, because dot on Windows does not seem
+# to support this out of the box.
+#
+# Warning: Depending on the platform used, enabling this option may lead to
+# badly anti-aliased labels on the edges of a graph (i.e. they become hard to
+# read).
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_TRANSPARENT        = NO
+
+# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
+# files in one run (i.e. multiple -o and -T options on the command line). This
+# makes dot run faster, but since only newer versions of dot (>1.8.10) support
+# this, this feature is disabled by default.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_MULTI_TARGETS      = NO
+
+# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
+# explaining the meaning of the various boxes and arrows in the dot generated
+# graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GENERATE_LEGEND        = YES
+
+# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate dot
+# files that are used to generate the various graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_CLEANUP            = YES
diff --git a/MANIFEST.in b/MANIFEST.in
index ce996f6c4ffcc6a70d095c24ed296ec3b69e5f43..96b265fd04394abbdbf033028b46c98619817869 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,5 @@
 include README.md
 include LICENSE
 include CMakeLists.txt
+include b_asic/GUI/operation_icons/*
 recursive-include src *.cpp *.h
diff --git a/README.md b/README.md
index fd98f919202588942a3e8d394b0b461ef63cfe54..db11db76c8a3053acb05f3430f2b19a4aedee5a5 100644
--- a/README.md
+++ b/README.md
@@ -9,15 +9,27 @@ How to build and debug the library during development.
 ### Prerequisites
 The following packages are required in order to build the library:
 * cmake 3.8+
-  * gcc 7+/clang 7+/msvc 16+
-  * fmtlib 5.2.1+
-  * pybind11 2.3.0+
+* gcc 7+/clang 7+/msvc 16+
+* fmtlib
+* pybind11 2.3.0+
 * python 3.6+
+* Python:
   * setuptools
-  * wheel
   * pybind11
   * numpy
-  * pyside2/pyqt5
+  * pyside2
+
+To build a binary distribution, the following additional packages are required:
+* Python:
+  * wheel
+
+To run the test suite, the following additional packages are required:
+* Python:
+  * pytest
+  * pytest-cov (for testing with coverage)
+  
+To generate the documentation, the following additional packages are required:
+* doxygen
 
 ### Using CMake directly
 How to build using CMake.
@@ -99,6 +111,13 @@ pytest
 pytest --cov=b_asic --cov-report html test
 ```
 
+### Generating documentation
+In `B-ASIC`:
+```
+doxygen
+```
+The output gets written to `B-ASIC/doc`.
+
 ## Usage
 How to build and use the library as a user.
 
diff --git a/b_asic/GUI/__init__.py b/b_asic/GUI/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..f330b58905bbebb411b1c96f877955b7ef644e28
--- /dev/null
+++ b/b_asic/GUI/__init__.py
@@ -0,0 +1,15 @@
+"""B-ASIC GUI Module.
+
+Graphical user interface for B-ASIC.
+"""
+from b_asic.GUI.main_window import *
+from b_asic.GUI.about_window import *
+from b_asic.GUI.arrow import *
+from b_asic.GUI.drag_button import *
+from b_asic.GUI.gui_interface import *
+from b_asic.GUI.port_button import *
+from b_asic.GUI.properties_window import *
+from b_asic.GUI.select_sfg_window import *
+from b_asic.GUI.show_pc_window import *
+from b_asic.GUI.simulate_sfg_window import *
+from b_asic.GUI.utils import *
diff --git a/b_asic/GUI/about_window.py b/b_asic/GUI/about_window.py
new file mode 100644
index 0000000000000000000000000000000000000000..699e1b7cbbd8e1b1929ad7fe44df7b88238526ae
--- /dev/null
+++ b/b_asic/GUI/about_window.py
@@ -0,0 +1,130 @@
+from PySide2.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget, QDialog, QLabel, QFrame, QScrollArea
+from PySide2.QtCore import Qt
+
+
+QUESTIONS = {
+    "Adding operations": "Select an operation under 'Special operations' or 'Core operations' to add it to the workspace.",
+    "Moving operations": "To drag an operation, select the operation on the workspace and drag it around.",
+    "Selecting operations": "To select one operation just press it once, it will then turn grey.",
+    "Selecting multiple operations using dragging": "To select multiple operations using your mouse, \ndrag the mouse while pressing left mouse button, any operation under the selection box will then be selected.",
+    "Selecting multiple operations using without dragging": "To select mutliple operations using without dragging, \npress 'Ctrl+LMouseButton' on any operation.",
+    "Remove operations": "To remove an operation, select the operation to be deleted, \nfinally press RMouseButton to bring up the context menu, then press 'Delete'.",
+    "Remove multiple operations": "To remove multiple operations, \nselect all operations to be deleted and press 'Delete' on your keyboard.",
+    "Connecting operations": "To connect operations, select the ports on the operation to connect from, \nthen select the next port by pressing 'Ctrl+LMouseButton' on the destination port. Tip: You can chain connection by selecting the ports in the order they should be connected.",
+    "Creating a signal-flow-graph": "To create a signal-flow-graph (SFG), \ncouple together the operations you wish to create a sfg from, then select all operations you wish to include in the sfg, \nfinally press 'Create SFG' in the upper left corner and enter the name of the sfg.",
+    "Simulating a signal-flow-graph": "To simulate a signal-flow-graph (SFG), press the run button in the toolbar, \nthen press 'Simulate SFG' and enter the properties of the simulation.",
+    "Properties of simulation": "The properties of the simulation are, 'Iteration Count': The number of iterations to run the simulation for, \n'Plot Results': Open a plot over the output in matplotlib, \n'Get All Results': Print the detailed output from simulating the sfg in the terminal, \n'Input Values': The input values to the SFG by index of the port."
+}
+
+
+class KeybindsWindow(QDialog):
+    def __init__(self, window):
+        super(KeybindsWindow, self).__init__()
+        self._window = window
+        self.setWindowFlags(Qt.WindowTitleHint | Qt.WindowCloseButtonHint)
+        self.setWindowTitle("B-ASIC Keybinds")
+
+        self.dialog_layout = QVBoxLayout()
+        self.setLayout(self.dialog_layout)
+
+        self.add_information_to_layout()
+
+    def add_information_to_layout(self):
+        information_layout = QVBoxLayout()
+
+        title_label = QLabel("B-ASIC / Better ASIC Toolbox")
+        subtitle_label = QLabel("Keybinds in the GUI.")
+
+        frame = QFrame()
+        frame.setFrameShape(QFrame.HLine)
+        frame.setFrameShadow(QFrame.Sunken)
+        self.dialog_layout.addWidget(frame)
+
+        keybinds_label = QLabel(
+            "'Ctrl+R' - Reload the operation list to add any new operations created.\n"
+            "'Ctrl+Q' - Quit the application.\n"
+            "'Ctrl+LMouseButton' - On a operation will select the operation, without deselecting the other operations.\n"
+            "'Ctrl+S' (Plot) - Save the plot if a plot is visible.\n"
+            "'Ctrl+?' - Open the FAQ section."
+        )
+
+        information_layout.addWidget(title_label)
+        information_layout.addWidget(subtitle_label)
+
+        self.dialog_layout.addLayout(information_layout)
+        self.dialog_layout.addWidget(frame)
+
+        self.dialog_layout.addWidget(keybinds_label)
+
+
+class AboutWindow(QDialog):
+    def __init__(self, window):
+        super(AboutWindow, self).__init__()
+        self._window = window
+        self.setWindowFlags(Qt.WindowTitleHint | Qt.WindowCloseButtonHint)
+        self.setWindowTitle("About B-ASIC")
+
+        self.dialog_layout = QVBoxLayout()
+        self.setLayout(self.dialog_layout)
+
+        self.add_information_to_layout()
+
+    def add_information_to_layout(self):
+        information_layout = QVBoxLayout()
+
+        title_label = QLabel("B-ASIC / Better ASIC Toolbox")
+        subtitle_label = QLabel("Construct, simulate and analyze components of an ASIC.")
+
+        frame = QFrame()
+        frame.setFrameShape(QFrame.HLine)
+        frame.setFrameShadow(QFrame.Sunken)
+        self.dialog_layout.addWidget(frame)
+
+        about_label = QLabel(
+            "B-ASIC is a open source tool using the B-ASIC library to construct, simulate and analyze ASICs.\n"
+            "B-ASIC is developed under the MIT-license and any extension to the program should follow that same license.\n"
+            "To read more about how the GUI works please refer to the FAQ under 'Help'."
+        )
+
+        information_layout.addWidget(title_label)
+        information_layout.addWidget(subtitle_label)
+
+        self.dialog_layout.addLayout(information_layout)
+        self.dialog_layout.addWidget(frame)
+
+        self.dialog_layout.addWidget(about_label)
+
+
+class FaqWindow(QDialog):
+    def __init__(self, window):
+        super(FaqWindow, self).__init__()
+        self._window = window
+        self.setWindowFlags(Qt.WindowTitleHint | Qt.WindowCloseButtonHint)
+        self.setWindowTitle("Frequently Asked Questions")
+
+        self.dialog_layout = QVBoxLayout()
+        self.scroll_area = QScrollArea()
+        self.setLayout(self.dialog_layout)
+        for question, answer in QUESTIONS.items():
+            self.add_question_to_layout(question, answer)
+
+        self.scroll_area.setWidget(self)
+        self.scroll_area.setWidgetResizable(True)
+
+    def add_question_to_layout(self, question, answer):
+        question_layout = QVBoxLayout()
+        answer_layout = QHBoxLayout()
+
+        question_label = QLabel(question)
+        question_layout.addWidget(question_label)
+
+        answer_label = QLabel(answer)
+        answer_layout.addWidget(answer_label)
+
+        frame = QFrame()
+        frame.setFrameShape(QFrame.HLine)
+        frame.setFrameShadow(QFrame.Sunken)
+        self.dialog_layout.addWidget(frame)
+
+        question_layout.addLayout(answer_layout)
+        self.dialog_layout.addLayout(question_layout)
diff --git a/b_asic/GUI/arrow.py b/b_asic/GUI/arrow.py
new file mode 100644
index 0000000000000000000000000000000000000000..7df52d052a209921b6592d3dd581a8fe3cefd83e
--- /dev/null
+++ b/b_asic/GUI/arrow.py
@@ -0,0 +1,49 @@
+from PySide2.QtWidgets import QApplication, QWidget, QMainWindow, QLabel, QAction,\
+QStatusBar, QMenuBar, QLineEdit, QPushButton, QSlider, QScrollArea, QVBoxLayout,\
+QHBoxLayout, QDockWidget, QToolBar, QMenu, QLayout, QSizePolicy, QListWidget, QListWidgetItem,\
+QGraphicsLineItem, QGraphicsWidget
+from PySide2.QtCore import Qt, QSize, QLineF, QPoint, QRectF
+from PySide2.QtGui import QIcon, QFont, QPainter, QPen
+
+from b_asic.signal import Signal
+
+class Arrow(QGraphicsLineItem):
+
+    def __init__(self, source, destination, window, signal=None, parent=None):
+        super(Arrow, self).__init__(parent)
+        self.source = source
+        self.signal = Signal(source.port, destination.port) if signal is None else signal
+        self.destination = destination
+        self._window = window
+        self.moveLine()
+        self.source.moved.connect(self.moveLine)
+        self.destination.moved.connect(self.moveLine)
+
+    def contextMenuEvent(self, event):
+        menu = QMenu()
+        menu.addAction("Delete", self.remove)
+        menu.exec_(self.cursor().pos())
+
+    def remove(self):
+        self.signal.remove_destination()
+        self.signal.remove_source()
+        self._window.scene.removeItem(self)
+        if self in self._window.signalList:
+            self._window.signalList.remove(self)
+
+        if self in self._window.signalPortDict:
+            for port1, port2 in self._window.signalPortDict[self]:
+                for operation, operation_ports in self._window.portDict.items():
+                    if (port1 in operation_ports or port2 in operation_ports) and operation in self._window.opToSFG:
+                        self._window.logger.info(f"Operation detected in existing sfg, removing sfg with name: {self._window.opToSFG[operation].name}.")
+                        del self._window.sfg_dict[self._window.opToSFG[operation].name]
+                        self._window.opToSFG = {op: self._window.opToSFG[op] for op in self._window.opToSFG if self._window.opToSFG[op] is not self._window.opToSFG[operation]}
+
+        del self._window.signalPortDict[self]
+
+    def moveLine(self):
+        self.setPen(QPen(Qt.black, 3))
+        self.setLine(QLineF(self.source.operation.x()+self.source.x()+14,\
+             self.source.operation.y()+self.source.y()+7.5,\
+             self.destination.operation.x()+self.destination.x(),\
+             self.destination.operation.y()+self.destination.y()+7.5))
diff --git a/b_asic/GUI/drag_button.py b/b_asic/GUI/drag_button.py
new file mode 100644
index 0000000000000000000000000000000000000000..a1d8118e094bd80d9c37c838e56b6746969b4453
--- /dev/null
+++ b/b_asic/GUI/drag_button.py
@@ -0,0 +1,156 @@
+"""B-ASIC Drag Button Module.
+
+Contains a GUI class for drag buttons.
+"""
+
+import os.path
+
+from b_asic.GUI.properties_window import PropertiesWindow
+from b_asic.GUI.utils import decorate_class, handle_error
+
+from PySide2.QtWidgets import QPushButton, QMenu, QAction
+from PySide2.QtCore import Qt, QSize, Signal
+from PySide2.QtGui import QIcon
+
+
+@decorate_class(handle_error)
+class DragButton(QPushButton):
+    """Drag button class.
+
+    This class creates a drag button which can be clicked, dragged and dropped.
+    """
+
+    connectionRequested = Signal(QPushButton)
+    moved = Signal()
+
+    def __init__(self, name, operation, operation_path_name, is_show_name, window, parent=None):
+        self.name = name
+        self.ports = []
+        self.is_show_name = is_show_name
+        self._window = window
+        self.operation = operation
+        self.operation_path_name = operation_path_name
+        self.clicked = 0
+        self.pressed = False
+        self._m_press = False
+        self._m_drag = False
+        self._mouse_press_pos = None
+        self._mouse_move_pos = None
+        super(DragButton, self).__init__(parent)
+
+    def contextMenuEvent(self, event):
+        menu = QMenu()
+        properties = QAction("Properties")
+        menu.addAction(properties)
+        properties.triggered.connect(self.show_properties_window)
+
+        delete = QAction("Delete")
+        menu.addAction(delete)
+        delete.triggered.connect(self.remove)
+        menu.exec_(self.cursor().pos())
+
+    def show_properties_window(self):
+        self.properties_window = PropertiesWindow(self, self._window)
+        self.properties_window.show()
+
+    def add_label(self, label):
+        self.label = label
+
+    def mousePressEvent(self, event):
+        if event.button() == Qt.LeftButton:
+            self._m_press = True
+            self._mouse_press_pos = event.pos()
+            self._mouse_move_pos = event.pos()
+
+        super(DragButton, self).mousePressEvent(event)
+
+    def mouseMoveEvent(self, event):
+        if event.buttons() == Qt.LeftButton and self._m_press:
+            self._m_drag = True
+            self.move(self.mapToParent(event.pos() - self._mouse_press_pos))
+            if self in self._window.pressed_operations:
+                for button in self._window.pressed_operations:
+                    if button is self:
+                        continue
+
+                    button.move(button.mapToParent(
+                        event.pos() - self._mouse_press_pos))
+
+        self._window.scene.update()
+        self._window.graphic_view.update()
+        super(DragButton, self).mouseMoveEvent(event)
+
+    def mouseReleaseEvent(self, event):
+        self._m_press = False
+        if self._m_drag:
+            if self._mouse_press_pos is not None:
+                moved = event.pos() - self._mouse_press_pos
+                if moved.manhattanLength() > 3:
+                    event.ignore()
+
+            self._m_drag = False
+
+        else:
+            self.select_button(event.modifiers())
+
+        super(DragButton, self).mouseReleaseEvent(event)
+
+    def _toggle_button(self, pressed=False):
+        self.pressed = not pressed
+        self.setStyleSheet(f"background-color: {'white' if not self.pressed else 'grey'}; border-style: solid;\
+        border-color: black; border-width: 2px")
+        path_to_image = os.path.join(os.path.dirname(
+            __file__), 'operation_icons', f"{self.operation_path_name}{'_grey.png' if self.pressed else '.png'}")
+        self.setIcon(QIcon(path_to_image))
+        self.setIconSize(QSize(55, 55))
+
+    def select_button(self, modifiers=None):
+        if modifiers != Qt.ControlModifier:
+            for button in self._window.pressed_operations:
+                button._toggle_button(button.pressed)
+
+            self._toggle_button(self.pressed)
+            self._window.pressed_operations = [self]
+
+        else:
+            self._toggle_button(self.pressed)
+            if self in self._window.pressed_operations:
+                self._window.pressed_operations.remove(self)
+            else:
+                self._window.pressed_operations.append(self)
+
+        for signal in self._window.signalList:
+            signal.update()
+
+    def remove(self):
+        self._window.logger.info(
+            f"Removing operation with name {self.operation.name}.")
+        self._window.scene.removeItem(
+            self._window.dragOperationSceneDict[self])
+
+        _signals = []
+        for signal, ports in self._window.signalPortDict.items():
+            if any(map(lambda port: set(port).intersection(set(self._window.portDict[self])), ports)):
+                self._window.logger.info(
+                    f"Removed signal with name: {signal.signal.name} to/from operation: {self.operation.name}.")
+                _signals.append(signal)
+
+        for signal in _signals:
+            signal.remove()
+
+        if self in self._window.opToSFG:
+            self._window.logger.info(
+                f"Operation detected in existing sfg, removing sfg with name: {self._window.opToSFG[self].name}.")
+            del self._window.sfg_dict[self._window.opToSFG[self].name]
+            self._window.opToSFG = {
+                op: self._window.opToSFG[op] for op in self._window.opToSFG if self._window.opToSFG[op] is not self._window.opToSFG[self]}
+
+        for port in self._window.portDict[self]:
+            if port in self._window.pressed_ports:
+                self._window.pressed_ports.remove(port)
+
+        if self in self._window.pressed_operations:
+            self._window.pressed_operations.remove(self)
+
+        if self in self._window.dragOperationSceneDict.keys():
+            del self._window.dragOperationSceneDict[self]
diff --git a/b_asic/GUI/gui_interface.py b/b_asic/GUI/gui_interface.py
new file mode 100644
index 0000000000000000000000000000000000000000..bfbe76f41595c93a48a9c35215ca70aad00a83ce
--- /dev/null
+++ b/b_asic/GUI/gui_interface.py
@@ -0,0 +1,326 @@
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'gui_interface.ui'
+#
+# Created by: PyQt5 UI code generator 5.14.2
+#
+# WARNING! All changes made in this file will be lost!
+
+
+from PySide2 import QtCore, QtGui, QtWidgets
+
+
+class Ui_main_window(object):
+    def setupUi(self, main_window):
+        main_window.setObjectName("main_window")
+        main_window.setEnabled(True)
+        main_window.resize(897, 633)
+        self.centralwidget = QtWidgets.QWidget(main_window)
+        self.centralwidget.setObjectName("centralwidget")
+        self.operation_box = QtWidgets.QGroupBox(self.centralwidget)
+        self.operation_box.setGeometry(QtCore.QRect(10, 10, 201, 531))
+        self.operation_box.setLayoutDirection(QtCore.Qt.LeftToRight)
+        self.operation_box.setAutoFillBackground(False)
+        self.operation_box.setStyleSheet("QGroupBox { \n"
+"     border: 2px solid gray; \n"
+"     border-radius: 3px;\n"
+"     margin-top: 0.5em; \n"
+" } \n"
+"\n"
+"QGroupBox::title {\n"
+"    subcontrol-origin: margin;\n"
+"    left: 10px;\n"
+"    padding: 0 3px 0 3px;\n"
+"}")
+        self.operation_box.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter)
+        self.operation_box.setFlat(False)
+        self.operation_box.setCheckable(False)
+        self.operation_box.setObjectName("operation_box")
+        self.operation_list = QtWidgets.QToolBox(self.operation_box)
+        self.operation_list.setGeometry(QtCore.QRect(10, 20, 171, 271))
+        self.operation_list.setAutoFillBackground(False)
+        self.operation_list.setObjectName("operation_list")
+        self.core_operations_page = QtWidgets.QWidget()
+        self.core_operations_page.setGeometry(QtCore.QRect(0, 0, 171, 217))
+        self.core_operations_page.setObjectName("core_operations_page")
+        self.core_operations_list = QtWidgets.QListWidget(self.core_operations_page)
+        self.core_operations_list.setGeometry(QtCore.QRect(10, 0, 141, 211))
+        self.core_operations_list.setMinimumSize(QtCore.QSize(141, 0))
+        self.core_operations_list.setEditTriggers(QtWidgets.QAbstractItemView.DoubleClicked|QtWidgets.QAbstractItemView.EditKeyPressed)
+        self.core_operations_list.setDragEnabled(False)
+        self.core_operations_list.setDragDropMode(QtWidgets.QAbstractItemView.NoDragDrop)
+        self.core_operations_list.setMovement(QtWidgets.QListView.Static)
+        self.core_operations_list.setFlow(QtWidgets.QListView.TopToBottom)
+        self.core_operations_list.setProperty("isWrapping", False)
+        self.core_operations_list.setResizeMode(QtWidgets.QListView.Adjust)
+        self.core_operations_list.setLayoutMode(QtWidgets.QListView.SinglePass)
+        self.core_operations_list.setViewMode(QtWidgets.QListView.ListMode)
+        self.core_operations_list.setUniformItemSizes(False)
+        self.core_operations_list.setWordWrap(False)
+        self.core_operations_list.setSelectionRectVisible(False)
+        self.core_operations_list.setObjectName("core_operations_list")
+        self.operation_list.addItem(self.core_operations_page, "")
+        self.special_operations_page = QtWidgets.QWidget()
+        self.special_operations_page.setGeometry(QtCore.QRect(0, 0, 171, 217))
+        self.special_operations_page.setObjectName("special_operations_page")
+        self.special_operations_list = QtWidgets.QListWidget(self.special_operations_page)
+        self.special_operations_list.setGeometry(QtCore.QRect(10, 0, 141, 211))
+        self.special_operations_list.setObjectName("special_operations_list")
+        self.operation_list.addItem(self.special_operations_page, "")
+        self.custom_operations_page = QtWidgets.QWidget()
+        self.custom_operations_page.setGeometry(QtCore.QRect(0, 0, 171, 217))
+        self.custom_operations_page.setObjectName("custom_operations_page")
+        self.custom_operations_list = QtWidgets.QListWidget(self.custom_operations_page)
+        self.custom_operations_list.setGeometry(QtCore.QRect(10, 0, 141, 211))
+        self.custom_operations_list.setObjectName("custom_operations_list")
+        self.operation_list.addItem(self.custom_operations_page, "")
+        main_window.setCentralWidget(self.centralwidget)
+        self.menu_bar = QtWidgets.QMenuBar(main_window)
+        self.menu_bar.setGeometry(QtCore.QRect(0, 0, 897, 21))
+        palette = QtGui.QPalette()
+        brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.WindowText, brush)
+        brush = QtGui.QBrush(QtGui.QColor(0, 255, 0))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)
+        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Light, brush)
+        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Midlight, brush)
+        brush = QtGui.QBrush(QtGui.QColor(127, 127, 127))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Dark, brush)
+        brush = QtGui.QBrush(QtGui.QColor(170, 170, 170))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Mid, brush)
+        brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Text, brush)
+        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.BrightText, brush)
+        brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ButtonText, brush)
+        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)
+        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)
+        brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Shadow, brush)
+        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.AlternateBase, brush)
+        brush = QtGui.QBrush(QtGui.QColor(255, 255, 220))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ToolTipBase, brush)
+        brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ToolTipText, brush)
+        brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 128))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.PlaceholderText, brush)
+        brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, brush)
+        brush = QtGui.QBrush(QtGui.QColor(0, 255, 0))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)
+        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Light, brush)
+        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Midlight, brush)
+        brush = QtGui.QBrush(QtGui.QColor(127, 127, 127))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Dark, brush)
+        brush = QtGui.QBrush(QtGui.QColor(170, 170, 170))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Mid, brush)
+        brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Text, brush)
+        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.BrightText, brush)
+        brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ButtonText, brush)
+        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)
+        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)
+        brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Shadow, brush)
+        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.AlternateBase, brush)
+        brush = QtGui.QBrush(QtGui.QColor(255, 255, 220))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipBase, brush)
+        brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipText, brush)
+        brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 128))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.PlaceholderText, brush)
+        brush = QtGui.QBrush(QtGui.QColor(127, 127, 127))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, brush)
+        brush = QtGui.QBrush(QtGui.QColor(0, 255, 0))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)
+        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Light, brush)
+        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Midlight, brush)
+        brush = QtGui.QBrush(QtGui.QColor(127, 127, 127))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Dark, brush)
+        brush = QtGui.QBrush(QtGui.QColor(170, 170, 170))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Mid, brush)
+        brush = QtGui.QBrush(QtGui.QColor(127, 127, 127))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Text, brush)
+        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.BrightText, brush)
+        brush = QtGui.QBrush(QtGui.QColor(127, 127, 127))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, brush)
+        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)
+        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)
+        brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Shadow, brush)
+        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.AlternateBase, brush)
+        brush = QtGui.QBrush(QtGui.QColor(255, 255, 220))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipBase, brush)
+        brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipText, brush)
+        brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 128))
+        brush.setStyle(QtCore.Qt.SolidPattern)
+        palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.PlaceholderText, brush)
+        self.menu_bar.setPalette(palette)
+        self.menu_bar.setObjectName("menu_bar")
+        self.file_menu = QtWidgets.QMenu(self.menu_bar)
+        self.file_menu.setObjectName("file_menu")
+        self.edit_menu = QtWidgets.QMenu(self.menu_bar)
+        self.edit_menu.setObjectName("edit_menu")
+        self.view_menu = QtWidgets.QMenu(self.menu_bar)
+        self.view_menu.setObjectName("view_menu")
+        self.run_menu = QtWidgets.QMenu(self.menu_bar)
+        self.run_menu.setObjectName("run_menu")
+        self.help_menu = QtWidgets.QMenu(self.menu_bar)
+        self.help_menu.setObjectName("help_menu")
+        main_window.setMenuBar(self.menu_bar)
+        self.status_bar = QtWidgets.QStatusBar(main_window)
+        self.status_bar.setObjectName("status_bar")
+        main_window.setStatusBar(self.status_bar)
+        self.load_menu = QtWidgets.QAction(main_window)
+        self.load_menu.setObjectName("load_menu")
+        self.save_menu = QtWidgets.QAction(main_window)
+        self.save_menu.setObjectName("save_menu")
+        self.load_operations = QtWidgets.QAction(main_window)
+        self.load_operations.setObjectName("load_operations")
+        self.exit_menu = QtWidgets.QAction(main_window)
+        self.exit_menu.setObjectName("exit_menu")
+        self.actionSimulateSFG = QtWidgets.QAction(main_window)
+        self.actionSimulateSFG.setObjectName("actionSimulateSFG")
+        self.actionShowPC = QtWidgets.QAction(main_window)
+        self.actionShowPC.setObjectName("actionShowPC")
+        self.aboutBASIC = QtWidgets.QAction(main_window)
+        self.aboutBASIC.setObjectName("aboutBASIC")
+        self.faqBASIC = QtWidgets.QAction(main_window)
+        self.faqBASIC.setObjectName("faqBASIC")
+        self.keybindsBASIC = QtWidgets.QAction(main_window)
+        self.keybindsBASIC.setObjectName("keybindsBASIC")
+        self.actionToolbar = QtWidgets.QAction(main_window)
+        self.actionToolbar.setCheckable(True)
+        self.actionToolbar.setObjectName("actionToolbar")
+        self.file_menu.addAction(self.load_menu)
+        self.file_menu.addAction(self.save_menu)
+        self.file_menu.addAction(self.load_operations)
+        self.file_menu.addSeparator()
+        self.file_menu.addAction(self.exit_menu)
+        self.view_menu.addAction(self.actionToolbar)
+        self.run_menu.addAction(self.actionShowPC)
+        self.run_menu.addAction(self.actionSimulateSFG)
+        self.help_menu.addAction(self.aboutBASIC)
+        self.help_menu.addAction(self.faqBASIC)
+        self.help_menu.addAction(self.keybindsBASIC)
+        self.menu_bar.addAction(self.file_menu.menuAction())
+        self.menu_bar.addAction(self.edit_menu.menuAction())
+        self.menu_bar.addAction(self.view_menu.menuAction())
+        self.menu_bar.addAction(self.run_menu.menuAction())
+        self.menu_bar.addAction(self.help_menu.menuAction())
+
+        self.retranslateUi(main_window)
+        self.operation_list.setCurrentIndex(1)
+        self.core_operations_list.setCurrentRow(-1)
+        QtCore.QMetaObject.connectSlotsByName(main_window)
+
+    def retranslateUi(self, main_window):
+        _translate = QtCore.QCoreApplication.translate
+        main_window.setWindowTitle(_translate("main_window", "B-ASIC"))
+        self.operation_box.setTitle(_translate("main_window", "Operations"))
+        self.core_operations_list.setSortingEnabled(False)
+        __sortingEnabled = self.core_operations_list.isSortingEnabled()
+        self.core_operations_list.setSortingEnabled(False)
+        self.core_operations_list.setSortingEnabled(__sortingEnabled)
+        self.operation_list.setItemText(self.operation_list.indexOf(self.core_operations_page), _translate("main_window", "Core operations"))
+        __sortingEnabled = self.special_operations_list.isSortingEnabled()
+        self.special_operations_list.setSortingEnabled(False)
+        self.special_operations_list.setSortingEnabled(__sortingEnabled)
+        self.operation_list.setItemText(self.operation_list.indexOf(self.special_operations_page), _translate("main_window", "Special operations"))
+        __sortingEnabled = self.special_operations_list.isSortingEnabled()
+        self.custom_operations_list.setSortingEnabled(False)
+        self.custom_operations_list.setSortingEnabled(__sortingEnabled)
+        self.operation_list.setItemText(self.operation_list.indexOf(self.custom_operations_page), _translate("main_window", "Custom operation"))
+        self.file_menu.setTitle(_translate("main_window", "File"))
+        self.edit_menu.setTitle(_translate("main_window", "Edit"))
+        self.view_menu.setTitle(_translate("main_window", "View"))
+        self.run_menu.setTitle(_translate("main_window", "Run"))
+        self.actionShowPC.setText(_translate("main_window", "Show PC"))
+        self.help_menu.setTitle(_translate("main_window", "Help"))
+        self.actionSimulateSFG.setText(_translate("main_window", "Simulate SFG"))
+        self.aboutBASIC.setText(_translate("main_window", "About B-ASIC"))
+        self.faqBASIC.setText(_translate("main_window", "FAQ"))
+        self.keybindsBASIC.setText(_translate("main_window", "Keybinds"))
+        self.load_menu.setText(_translate("main_window", "Load SFG"))
+        self.save_menu.setText(_translate("main_window", "Save SFG"))
+        self.load_operations.setText(_translate("main_window", "Load Operations"))
+        self.exit_menu.setText(_translate("main_window", "Exit"))
+        self.exit_menu.setShortcut(_translate("main_window", "Ctrl+Q"))
+        self.actionToolbar.setText(_translate("main_window", "Toolbar"))
+
+
+if __name__ == "__main__":
+    import sys
+    app = QtWidgets.QApplication(sys.argv)
+    main_window = QtWidgets.QMainWindow()
+    ui = Ui_main_window()
+    ui.setupUi(main_window)
+    main_window.show()
+    sys.exit(app.exec_())
diff --git a/b_asic/GUI/gui_interface.ui b/b_asic/GUI/gui_interface.ui
new file mode 100644
index 0000000000000000000000000000000000000000..382747818a3286373b825e4115c9b283799156bc
--- /dev/null
+++ b/b_asic/GUI/gui_interface.ui
@@ -0,0 +1,763 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>main_window</class>
+ <widget class="QMainWindow" name="main_window">
+  <property name="enabled">
+   <bool>true</bool>
+  </property>
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>897</width>
+    <height>633</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>MainWindow</string>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <widget class="QGroupBox" name="operation_box">
+    <property name="geometry">
+     <rect>
+      <x>10</x>
+      <y>10</y>
+      <width>201</width>
+      <height>531</height>
+     </rect>
+    </property>
+    <property name="layoutDirection">
+     <enum>Qt::LeftToRight</enum>
+    </property>
+    <property name="autoFillBackground">
+     <bool>false</bool>
+    </property>
+    <property name="styleSheet">
+     <string notr="true">QGroupBox { 
+     border: 2px solid gray; 
+     border-radius: 3px;
+	 margin-top: 0.5em; 
+ } 
+
+QGroupBox::title {
+    subcontrol-origin: margin;
+    left: 10px;
+    padding: 0 3px 0 3px;
+}</string>
+    </property>
+    <property name="title">
+     <string>Operations</string>
+    </property>
+    <property name="alignment">
+     <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+    </property>
+    <property name="flat">
+     <bool>false</bool>
+    </property>
+    <property name="checkable">
+     <bool>false</bool>
+    </property>
+    <widget class="QToolBox" name="operation_list">
+     <property name="geometry">
+      <rect>
+       <x>10</x>
+       <y>20</y>
+       <width>171</width>
+       <height>271</height>
+      </rect>
+     </property>
+     <property name="autoFillBackground">
+      <bool>false</bool>
+     </property>
+     <property name="currentIndex">
+      <number>1</number>
+     </property>
+     <widget class="QWidget" name="core_operations_page">
+      <property name="geometry">
+       <rect>
+        <x>0</x>
+        <y>0</y>
+        <width>171</width>
+        <height>217</height>
+       </rect>
+      </property>
+      <attribute name="label">
+       <string>Core operations</string>
+      </attribute>
+      <widget class="QListWidget" name="core_operations_list">
+       <property name="geometry">
+        <rect>
+         <x>10</x>
+         <y>0</y>
+         <width>141</width>
+         <height>211</height>
+        </rect>
+       </property>
+       <property name="minimumSize">
+        <size>
+         <width>141</width>
+         <height>0</height>
+        </size>
+       </property>
+       <property name="editTriggers">
+        <set>QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed</set>
+       </property>
+       <property name="dragEnabled">
+        <bool>false</bool>
+       </property>
+       <property name="dragDropMode">
+        <enum>QAbstractItemView::NoDragDrop</enum>
+       </property>
+       <property name="movement">
+        <enum>QListView::Static</enum>
+       </property>
+       <property name="flow">
+        <enum>QListView::TopToBottom</enum>
+       </property>
+       <property name="isWrapping" stdset="0">
+        <bool>false</bool>
+       </property>
+       <property name="resizeMode">
+        <enum>QListView::Adjust</enum>
+       </property>
+       <property name="layoutMode">
+        <enum>QListView::SinglePass</enum>
+       </property>
+       <property name="viewMode">
+        <enum>QListView::ListMode</enum>
+       </property>
+       <property name="uniformItemSizes">
+        <bool>false</bool>
+       </property>
+       <property name="wordWrap">
+        <bool>false</bool>
+       </property>
+       <property name="selectionRectVisible">
+        <bool>false</bool>
+       </property>
+       <property name="currentRow">
+        <number>-1</number>
+       </property>
+       <property name="sortingEnabled">
+        <bool>false</bool>
+       </property>
+       <item>
+        <property name="text">
+         <string>Addition</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Subtraction</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Multiplication</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Division</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Constant</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Constant multiplication</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Square root</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Complex conjugate</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Absolute</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Max</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Min</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Butterfly</string>
+        </property>
+       </item>
+      </widget>
+     </widget>
+     <widget class="QWidget" name="special_operations_page">
+      <property name="geometry">
+       <rect>
+        <x>0</x>
+        <y>0</y>
+        <width>171</width>
+        <height>217</height>
+       </rect>
+      </property>
+      <attribute name="label">
+       <string>Special operations</string>
+      </attribute>
+      <widget class="QListWidget" name="special_operations_list">
+       <property name="geometry">
+        <rect>
+         <x>10</x>
+         <y>0</y>
+         <width>141</width>
+         <height>81</height>
+        </rect>
+       </property>
+       <item>
+        <property name="text">
+         <string>Input</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Output</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Delay</string>
+        </property>
+       </item>
+       <item>
+        <property name="text">
+         <string>Custom</string>
+        </property>
+       </item>
+      </widget>
+     </widget>
+    </widget>
+   </widget>
+  </widget>
+  <widget class="QMenuBar" name="menu_bar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>897</width>
+     <height>21</height>
+    </rect>
+   </property>
+   <property name="palette">
+    <palette>
+     <active>
+      <colorrole role="WindowText">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>0</red>
+         <green>0</green>
+         <blue>0</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Button">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>0</red>
+         <green>255</green>
+         <blue>0</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Light">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>255</red>
+         <green>255</green>
+         <blue>255</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Midlight">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>255</red>
+         <green>255</green>
+         <blue>255</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Dark">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>127</red>
+         <green>127</green>
+         <blue>127</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Mid">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>170</red>
+         <green>170</green>
+         <blue>170</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Text">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>0</red>
+         <green>0</green>
+         <blue>0</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="BrightText">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>255</red>
+         <green>255</green>
+         <blue>255</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="ButtonText">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>0</red>
+         <green>0</green>
+         <blue>0</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Base">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>255</red>
+         <green>255</green>
+         <blue>255</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Window">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>255</red>
+         <green>255</green>
+         <blue>255</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Shadow">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>0</red>
+         <green>0</green>
+         <blue>0</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="AlternateBase">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>255</red>
+         <green>255</green>
+         <blue>255</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="ToolTipBase">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>255</red>
+         <green>255</green>
+         <blue>220</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="ToolTipText">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>0</red>
+         <green>0</green>
+         <blue>0</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="PlaceholderText">
+       <brush brushstyle="SolidPattern">
+        <color alpha="128">
+         <red>0</red>
+         <green>0</green>
+         <blue>0</blue>
+        </color>
+       </brush>
+      </colorrole>
+     </active>
+     <inactive>
+      <colorrole role="WindowText">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>0</red>
+         <green>0</green>
+         <blue>0</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Button">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>0</red>
+         <green>255</green>
+         <blue>0</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Light">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>255</red>
+         <green>255</green>
+         <blue>255</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Midlight">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>255</red>
+         <green>255</green>
+         <blue>255</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Dark">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>127</red>
+         <green>127</green>
+         <blue>127</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Mid">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>170</red>
+         <green>170</green>
+         <blue>170</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Text">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>0</red>
+         <green>0</green>
+         <blue>0</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="BrightText">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>255</red>
+         <green>255</green>
+         <blue>255</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="ButtonText">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>0</red>
+         <green>0</green>
+         <blue>0</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Base">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>255</red>
+         <green>255</green>
+         <blue>255</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Window">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>255</red>
+         <green>255</green>
+         <blue>255</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Shadow">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>0</red>
+         <green>0</green>
+         <blue>0</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="AlternateBase">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>255</red>
+         <green>255</green>
+         <blue>255</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="ToolTipBase">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>255</red>
+         <green>255</green>
+         <blue>220</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="ToolTipText">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>0</red>
+         <green>0</green>
+         <blue>0</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="PlaceholderText">
+       <brush brushstyle="SolidPattern">
+        <color alpha="128">
+         <red>0</red>
+         <green>0</green>
+         <blue>0</blue>
+        </color>
+       </brush>
+      </colorrole>
+     </inactive>
+     <disabled>
+      <colorrole role="WindowText">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>127</red>
+         <green>127</green>
+         <blue>127</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Button">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>0</red>
+         <green>255</green>
+         <blue>0</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Light">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>255</red>
+         <green>255</green>
+         <blue>255</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Midlight">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>255</red>
+         <green>255</green>
+         <blue>255</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Dark">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>127</red>
+         <green>127</green>
+         <blue>127</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Mid">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>170</red>
+         <green>170</green>
+         <blue>170</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Text">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>127</red>
+         <green>127</green>
+         <blue>127</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="BrightText">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>255</red>
+         <green>255</green>
+         <blue>255</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="ButtonText">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>127</red>
+         <green>127</green>
+         <blue>127</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Base">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>255</red>
+         <green>255</green>
+         <blue>255</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Window">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>255</red>
+         <green>255</green>
+         <blue>255</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="Shadow">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>0</red>
+         <green>0</green>
+         <blue>0</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="AlternateBase">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>255</red>
+         <green>255</green>
+         <blue>255</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="ToolTipBase">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>255</red>
+         <green>255</green>
+         <blue>220</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="ToolTipText">
+       <brush brushstyle="SolidPattern">
+        <color alpha="255">
+         <red>0</red>
+         <green>0</green>
+         <blue>0</blue>
+        </color>
+       </brush>
+      </colorrole>
+      <colorrole role="PlaceholderText">
+       <brush brushstyle="SolidPattern">
+        <color alpha="128">
+         <red>0</red>
+         <green>0</green>
+         <blue>0</blue>
+        </color>
+       </brush>
+      </colorrole>
+     </disabled>
+    </palette>
+   </property>
+   <widget class="QMenu" name="file_menu">
+    <property name="title">
+     <string>File</string>
+    </property>
+    <addaction name="save_menu"/>
+    <addaction name="separator"/>
+    <addaction name="exit_menu"/>
+   </widget>
+   <widget class="QMenu" name="edit_menu">
+    <property name="title">
+     <string>Edit</string>
+    </property>
+    <addaction name="actionUndo"/>
+    <addaction name="actionRedo"/>
+   </widget>
+   <widget class="QMenu" name="view_menu">
+    <property name="title">
+     <string>View</string>
+    </property>
+    <addaction name="actionToolbar"/>
+   </widget>
+   <addaction name="file_menu"/>
+   <addaction name="edit_menu"/>
+   <addaction name="view_menu"/>
+  </widget>
+  <widget class="QStatusBar" name="status_bar"/>
+  <action name="save_menu">
+   <property name="text">
+    <string>Save</string>
+   </property>
+  </action>
+  <action name="exit_menu">
+   <property name="text">
+    <string>Exit</string>
+   </property>
+   <property name="shortcut">
+    <string>Ctrl+Q</string>
+   </property>
+  </action>
+  <action name="actionUndo">
+   <property name="text">
+    <string>Undo</string>
+   </property>
+  </action>
+  <action name="actionRedo">
+   <property name="text">
+    <string>Redo</string>
+   </property>
+  </action>
+  <action name="actionToolbar">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Toolbar</string>
+   </property>
+  </action>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/b_asic/GUI/main_window.py b/b_asic/GUI/main_window.py
new file mode 100644
index 0000000000000000000000000000000000000000..630e4af04affe5be9e84559d52d58ec99092bf41
--- /dev/null
+++ b/b_asic/GUI/main_window.py
@@ -0,0 +1,625 @@
+"""B-ASIC Main Window Module.
+
+This file opens the main window of the GUI for B-ASIC when run.
+"""
+
+from tkinter.filedialog import askopenfilename, askopenfile
+from tkinter import Tk
+from PySide2.QtGui import QIcon, QFont, QPainter, QPen, QBrush, QKeySequence
+from PySide2.QtCore import Qt, QSize, QFileInfo
+from PySide2.QtWidgets import QApplication, QWidget, QMainWindow, QLabel, QAction,\
+    QStatusBar, QMenuBar, QLineEdit, QPushButton, QSlider, QScrollArea, QVBoxLayout,\
+    QHBoxLayout, QDockWidget, QToolBar, QMenu, QLayout, QSizePolicy, QListWidget,\
+    QListWidgetItem, QGraphicsView, QGraphicsScene, QShortcut, QGraphicsTextItem,\
+    QGraphicsProxyWidget, QInputDialog, QTextEdit, QFileDialog
+from numpy import linspace
+from b_asic.save_load_structure import *
+from select_sfg_window import SelectSFGWindow
+from simulate_sfg_window import SimulateSFGWindow, Plot
+from utils import decorate_class, handle_error
+import b_asic.special_operations as s_oper
+import b_asic.core_operations as c_oper
+from b_asic import Operation, SFG, InputPort, OutputPort, Input, Output, FastSimulation
+from b_asic.simulation import Simulation
+from show_pc_window import ShowPCWindow
+from port_button import PortButton
+from arrow import Arrow
+from gui_interface import Ui_main_window
+from drag_button import DragButton
+from about_window import AboutWindow, FaqWindow, KeybindsWindow
+import sys
+from pprint import pprint
+from os import getcwd, path
+import importlib
+import logging
+import sys
+
+from b_asic.GUI.about_window import AboutWindow, FaqWindow, KeybindsWindow
+from b_asic.GUI.drag_button import DragButton
+from b_asic.GUI.gui_interface import Ui_main_window
+from b_asic.GUI.arrow import Arrow
+from b_asic.GUI.port_button import PortButton
+from b_asic.GUI.show_pc_window import ShowPCWindow
+from b_asic.GUI.utils import decorate_class, handle_error
+from b_asic.GUI.simulate_sfg_window import SimulateSFGWindow, Plot
+from b_asic.GUI.select_sfg_window import SelectSFGWindow
+
+from b_asic import FastSimulation
+from b_asic.simulation import Simulation
+from b_asic.operation import Operation
+from b_asic.port import InputPort, OutputPort
+from b_asic.signal_flow_graph import SFG
+from b_asic.special_operations import Input, Output
+import b_asic.core_operations as c_oper
+import b_asic.special_operations as s_oper
+from b_asic.save_load_structure import *
+
+from numpy import linspace
+
+from PySide2.QtWidgets import QApplication, QWidget, QMainWindow, QLabel, QAction,\
+    QStatusBar, QMenuBar, QLineEdit, QPushButton, QSlider, QScrollArea, QVBoxLayout,\
+    QHBoxLayout, QDockWidget, QToolBar, QMenu, QLayout, QSizePolicy, QListWidget,\
+    QListWidgetItem, QGraphicsView, QGraphicsScene, QShortcut, QGraphicsTextItem,\
+    QGraphicsProxyWidget, QInputDialog, QTextEdit, QFileDialog
+from PySide2.QtCore import Qt, QSize, QFileInfo
+from PySide2.QtGui import QIcon, QFont, QPainter, QPen, QBrush, QKeySequence
+
+
+MIN_WIDTH_SCENE = 600
+MIN_HEIGHT_SCENE = 520
+logging.basicConfig(level=logging.INFO)
+
+
+@decorate_class(handle_error)
+class MainWindow(QMainWindow):
+    def __init__(self):
+        super(MainWindow, self).__init__()
+        self.ui = Ui_main_window()
+        self.ui.setupUi(self)
+        self.setWindowIcon(QIcon('small_logo.png'))
+        self.scene = None
+        self._operations_from_name = dict()
+        self.zoom = 1
+        self.sfg_name_i = 0
+        self.dragOperationSceneDict = dict()
+        self.operationDragDict = dict()
+        self.operationItemSceneList = []
+        self.signalList = []
+        self.pressed_operations = []
+        self.portDict = dict()
+        self.signalPortDict = dict()
+        self.opToSFG = dict()
+        self.pressed_ports = []
+        self.sfg_dict = dict()
+        self._window = self
+        self.logger = logging.getLogger(__name__)
+        self.init_ui()
+        self.add_operations_from_namespace(
+            c_oper, self.ui.core_operations_list)
+        self.add_operations_from_namespace(
+            s_oper, self.ui.special_operations_list)
+
+        self.shortcut_core = QShortcut(
+            QKeySequence("Ctrl+R"), self.ui.operation_box)
+        self.shortcut_core.activated.connect(
+            self._refresh_operations_list_from_namespace)
+        self.scene.selectionChanged.connect(self._select_operations)
+
+        self.move_button_index = 0
+        self.is_show_names = True
+
+        self.check_show_names = QAction("Show operation names")
+        self.check_show_names.triggered.connect(self.view_operation_names)
+        self.check_show_names.setCheckable(True)
+        self.check_show_names.setChecked(1)
+        self.ui.view_menu.addAction(self.check_show_names)
+
+        self.ui.actionShowPC.triggered.connect(self.show_precedence_chart)
+        self.ui.actionSimulateSFG.triggered.connect(self.simulate_sfg)
+        self.ui.faqBASIC.triggered.connect(self.display_faq_page)
+        self.ui.aboutBASIC.triggered.connect(self.display_about_page)
+        self.ui.keybindsBASIC.triggered.connect(self.display_keybinds_page)
+        self.ui.core_operations_list.itemClicked.connect(
+            self.on_list_widget_item_clicked)
+        self.ui.special_operations_list.itemClicked.connect(
+            self.on_list_widget_item_clicked)
+        self.ui.custom_operations_list.itemClicked.connect(
+            self.on_list_widget_item_clicked)
+        self.ui.save_menu.triggered.connect(self.save_work)
+        self.ui.load_menu.triggered.connect(self.load_work)
+        self.ui.load_operations.triggered.connect(self.add_namespace)
+        self.ui.exit_menu.triggered.connect(self.exit_app)
+        self.shortcut_open = QShortcut(QKeySequence("Ctrl+O"), self)
+        self.shortcut_open.activated.connect(self.load_work)
+        self.shortcut_save = QShortcut(QKeySequence("Ctrl+S"), self)
+        self.shortcut_save.activated.connect(self.save_work)
+        self.shortcut_help = QShortcut(QKeySequence("Ctrl+?"), self)
+        self.shortcut_help.activated.connect(self.display_faq_page)
+        self.shortcut_signal = QShortcut(QKeySequence(Qt.Key_Space), self)
+        self.shortcut_signal.activated.connect(self._connect_button)
+
+        self.logger.info("Finished setting up GUI")
+        self.logger.info(
+            "For questions please refer to 'Ctrl+?', or visit the 'Help' section on the toolbar.")
+
+    def init_ui(self):
+        self.create_toolbar_view()
+        self.create_graphics_view()
+
+    def create_graphics_view(self):
+        self.scene = QGraphicsScene(self)
+        self.graphic_view = QGraphicsView(self.scene, self)
+        self.graphic_view.setRenderHint(QPainter.Antialiasing)
+        self.graphic_view.setGeometry(
+            self.ui.operation_box.width(), 20, self.width(), self.height())
+        self.graphic_view.setDragMode(QGraphicsView.RubberBandDrag)
+
+    def create_toolbar_view(self):
+        self.toolbar = self.addToolBar("Toolbar")
+        self.toolbar.addAction("Create SFG", self.create_SFG_from_toolbar)
+        self.toolbar.addAction("Clear Workspace", self.clear_workspace)
+
+    def resizeEvent(self, event):
+        self.ui.operation_box.setGeometry(
+            10, 10, self.ui.operation_box.width(), self.height())
+        self.graphic_view.setGeometry(self.ui.operation_box.width(
+        ) + 20, 60, self.width() - self.ui.operation_box.width() - 20, self.height()-30)
+        super(MainWindow, self).resizeEvent(event)
+
+    def wheelEvent(self, event):
+        if event.modifiers() == Qt.ControlModifier:
+            old_zoom = self.zoom
+            self.zoom += event.angleDelta().y()/2500
+            self.graphic_view.scale(self.zoom, self.zoom)
+            self.zoom = old_zoom
+
+    def view_operation_names(self):
+        if self.check_show_names.isChecked():
+            self.is_show_names = True
+        else:
+            self.is_show_names = False
+
+        for operation in self.dragOperationSceneDict.keys():
+            operation.label.setOpacity(self.is_show_names)
+            operation.is_show_name = self.is_show_names
+
+    def _save_work(self):
+        sfg = self.sfg_widget.sfg
+        file_dialog = QFileDialog()
+        file_dialog.setDefaultSuffix(".py")
+        module, accepted = file_dialog.getSaveFileName()
+        if not accepted:
+            return
+
+        self.logger.info(f"Saving sfg to path: {module}.")
+        operation_positions = dict()
+        for operation_drag, operation_scene in self.dragOperationSceneDict.items():
+            operation_positions[operation_drag.operation.graph_id] = (
+                operation_scene.x(), operation_scene.y())
+
+        try:
+            with open(module, "w+") as file_obj:
+                file_obj.write(sfg_to_python(
+                    sfg, suffix=f"positions = {str(operation_positions)}"))
+        except Exception as e:
+            self.logger.error(
+                f"Failed to save sfg to path: {module}, with error: {e}.")
+            return
+
+        self.logger.info(f"Saved sfg to path: {module}.")
+
+    def save_work(self):
+        self.sfg_widget = SelectSFGWindow(self)
+        self.sfg_widget.show()
+
+        # Wait for input to dialog.
+        self.sfg_widget.ok.connect(self._save_work)
+
+    def load_work(self):
+        module, accepted = QFileDialog().getOpenFileName()
+        if not accepted:
+            return
+
+        self.logger.info(f"Loading sfg from path: {module}.")
+        try:
+            sfg, positions = python_to_sfg(module)
+        except ImportError as e:
+            self.logger.error(
+                f"Failed to load module: {module} with the following error: {e}.")
+            return
+
+        while sfg.name in self.sfg_dict:
+            self.logger.warning(
+                f"Duplicate sfg with name: {sfg.name} detected. Please choose a new name.")
+            name, accepted = QInputDialog.getText(
+                self, "Change SFG Name", "Name: ", QLineEdit.Normal)
+            if not accepted:
+                return
+
+            sfg.name = name
+
+        for op in sfg.split():
+            self.create_operation(
+                op, positions[op.graph_id] if op.graph_id in positions else None)
+
+        def connect_ports(ports):
+            for port in ports:
+                for signal in port.signals:
+                    source = [source for source in self.portDict[self.operationDragDict[signal.source.operation]]
+                              if source.port is signal.source]
+                    destination = [destination for destination in self.portDict[self.operationDragDict[
+                        signal.destination.operation]] if destination.port is signal.destination]
+
+                    if source and destination:
+                        self.connect_button(source[0], destination[0])
+
+            for port in self.pressed_ports:
+                port.select_port()
+
+        for op in sfg.split():
+            connect_ports(op.inputs)
+
+        for op in sfg.split():
+            self.operationDragDict[op].setToolTip(sfg.name)
+            self.opToSFG[self.operationDragDict[op]] = sfg
+
+        self.sfg_dict[sfg.name] = sfg
+        self.logger.info(f"Loaded sfg from path: {module}.")
+        self.update()
+
+    def exit_app(self):
+        self.logger.info("Exiting the application.")
+        QApplication.quit()
+
+    def clear_workspace(self):
+        self.logger.info("Clearing workspace from operations and sfgs.")
+        self.pressed_operations.clear()
+        self.pressed_ports.clear()
+        self.operationItemSceneList.clear()
+        self.operationDragDict.clear()
+        self.dragOperationSceneDict.clear()
+        self.signalList.clear()
+        self.portDict.clear()
+        self.signalPortDict.clear()
+        self.sfg_dict.clear()
+        self.scene.clear()
+        self.logger.info("Workspace cleared.")
+
+    def create_SFG_from_toolbar(self):
+        inputs = []
+        outputs = []
+        for op in self.pressed_operations:
+            if isinstance(op.operation, Input):
+                inputs.append(op.operation)
+            elif isinstance(op.operation, Output):
+                outputs.append(op.operation)
+
+        name, accepted = QInputDialog.getText(
+            self, "Create SFG", "Name: ", QLineEdit.Normal)
+        if not accepted:
+            return
+
+        if name == "":
+            self.logger.warning(f"Failed to initialize SFG with empty name.")
+            return
+
+        self.logger.info(
+            f"Creating SFG with name: {name} from selected operations.")
+
+        sfg = SFG(inputs=inputs, outputs=outputs, name=name)
+        self.logger.info(
+            f"Created SFG with name: {name} from selected operations.")
+
+        def check_equality(signal, signal_2):
+            if not (signal.source.operation.type_name() == signal_2.source.operation.type_name()
+                    and signal.destination.operation.type_name() == signal_2.destination.operation.type_name()):
+                return False
+
+            if hasattr(signal.source.operation, "value") and hasattr(signal_2.source.operation, "value") \
+                    and hasattr(signal.destination.operation, "value") and hasattr(signal_2.destination.operation, "value"):
+                if not (signal.source.operation.value == signal_2.source.operation.value
+                        and signal.destination.operation.value == signal_2.destination.operation.value):
+                    return False
+
+            if hasattr(signal.source.operation, "name") and hasattr(signal_2.source.operation, "name") \
+                    and hasattr(signal.destination.operation, "name") and hasattr(signal_2.destination.operation, "name"):
+                if not (signal.source.operation.name == signal_2.source.operation.name
+                        and signal.destination.operation.name == signal_2.destination.operation.name):
+                    return False
+
+            try:
+                _signal_source_index = [signal.source.operation.outputs.index(
+                    port) for port in signal.source.operation.outputs if signal in port.signals]
+                _signal_2_source_index = [signal_2.source.operation.outputs.index(
+                    port) for port in signal_2.source.operation.outputs if signal_2 in port.signals]
+            except ValueError:
+                return False  # Signal output connections not matching
+
+            try:
+                _signal_destination_index = [signal.destination.operation.inputs.index(
+                    port) for port in signal.destination.operation.inputs if signal in port.signals]
+                _signal_2_destination_index = [signal_2.destination.operation.inputs.index(
+                    port) for port in signal_2.destination.operation.inputs if signal_2 in port.signals]
+            except ValueError:
+                return False  # Signal input connections not matching
+
+            if not (_signal_source_index == _signal_2_source_index and _signal_destination_index == _signal_2_destination_index):
+                return False
+
+            return True
+
+        for pressed_op in self.pressed_operations:
+            for operation in sfg.operations:
+                for input_ in operation.inputs:
+                    for signal in input_.signals:
+                        for line in self.signalPortDict:
+                            if check_equality(line.signal, signal):
+                                line.source.operation.operation = signal.source.operation
+                                line.destination.operation.operation = signal.destination.operation
+
+                for output_ in operation.outputs:
+                    for signal in output_.signals:
+                        for line in self.signalPortDict:
+                            if check_equality(line.signal, signal):
+                                line.source.operation.operation = signal.source.operation
+                                line.destination.operation.operation = signal.destination.operation
+
+        for op in self.pressed_operations:
+            op.setToolTip(sfg.name)
+            self.opToSFG[op] = sfg
+
+        self.sfg_dict[sfg.name] = sfg
+
+    def show_precedence_chart(self):
+        self.dialog = ShowPCWindow(self)
+        self.dialog.add_sfg_to_dialog()
+        self.dialog.show()
+
+    def _determine_port_distance(self, length, ports):
+        """Determine the distance between each port on the side of an operation.
+        The method returns the distance that each port should have from 0.
+        """
+        return [length / 2] if ports == 1 else linspace(0, length, ports)
+
+    def add_ports(self, operation):
+        _output_ports_dist = self._determine_port_distance(
+            55 - 17, operation.operation.output_count)
+        _input_ports_dist = self._determine_port_distance(
+            55 - 17, operation.operation.input_count)
+        self.portDict[operation] = list()
+
+        for i, dist in enumerate(_input_ports_dist):
+            port = PortButton(
+                ">", operation, operation.operation.input(i), self)
+            self.portDict[operation].append(port)
+            operation.ports.append(port)
+            port.move(0, dist)
+            port.show()
+
+        for i, dist in enumerate(_output_ports_dist):
+            port = PortButton(
+                ">", operation, operation.operation.output(i), self)
+            self.portDict[operation].append(port)
+            operation.ports.append(port)
+            port.move(55 - 12, dist)
+            port.show()
+
+    def get_operations_from_namespace(self, namespace):
+        self.logger.info(
+            f"Fetching operations from namespace: {namespace.__name__}.")
+        return [comp for comp in dir(namespace) if hasattr(getattr(namespace, comp), "type_name")]
+
+    def add_operations_from_namespace(self, namespace, _list):
+        for attr_name in self.get_operations_from_namespace(namespace):
+            attr = getattr(namespace, attr_name)
+            try:
+                attr.type_name()
+                item = QListWidgetItem(attr_name)
+                _list.addItem(item)
+                self._operations_from_name[attr_name] = attr
+            except NotImplementedError:
+                pass
+
+        self.logger.info(
+            f"Added operations from namespace: {namespace.__name__}.")
+
+    def add_namespace(self):
+        module, accepted = QFileDialog().getOpenFileName()
+        if not accepted:
+            return
+
+        spec = importlib.util.spec_from_file_location(
+            f"{QFileInfo(module).fileName()}", module)
+        namespace = importlib.util.module_from_spec(spec)
+        spec.loader.exec_module(namespace)
+
+        self.add_operations_from_namespace(
+            namespace, self.ui.custom_operations_list)
+
+    def create_operation(self, op, position=None):
+        try:
+            attr_button = DragButton(
+                op.graph_id, op, op.type_name().lower(), True, window=self)
+            if position is None:
+                attr_button.move(250, 100)
+            else:
+                attr_button.move(*position)
+
+            attr_button.setFixedSize(55, 55)
+            attr_button.setStyleSheet("background-color: white; border-style: solid;\
+            border-color: black; border-width: 2px")
+            self.add_ports(attr_button)
+
+            icon_path = path.join(path.dirname(
+                __file__), "operation_icons", f"{op.type_name().lower()}.png")
+            if not path.exists(icon_path):
+                icon_path = path.join(path.dirname(
+                    __file__), "operation_icons", f"custom_operation.png")
+            attr_button.setIcon(QIcon(icon_path))
+            attr_button.setIconSize(QSize(55, 55))
+            attr_button.setToolTip("No sfg")
+            attr_button.setStyleSheet(""" QToolTip { background-color: white;
+            color: black }""")
+            attr_button.setParent(None)
+            attr_button_scene = self.scene.addWidget(attr_button)
+            if position is None:
+                attr_button_scene.moveBy(
+                    int(self.scene.width() / 2), int(self.scene.height() / 2))
+            attr_button_scene.setFlag(attr_button_scene.ItemIsSelectable, True)
+            operation_label = QGraphicsTextItem(op.name, attr_button_scene)
+            if not self.is_show_names:
+                operation_label.setOpacity(0)
+            operation_label.setTransformOriginPoint(
+                operation_label.boundingRect().center())
+            operation_label.moveBy(10, -20)
+            attr_button.add_label(operation_label)
+            self.operationDragDict[op] = attr_button
+            self.dragOperationSceneDict[attr_button] = attr_button_scene
+        except Exception as e:
+            self.logger.error(
+                f"Unexpected error occured while creating operation: {e}.")
+
+    def _create_operation_item(self, item):
+        self.logger.info(f"Creating operation of type: {item.text()}.")
+        try:
+            attr_oper = self._operations_from_name[item.text()]()
+            self.create_operation(attr_oper)
+        except Exception as e:
+            self.logger.error(
+                f"Unexpected error occured while creating operation: {e}.")
+
+    def _refresh_operations_list_from_namespace(self):
+        self.logger.info("Refreshing operation list.")
+        self.ui.core_operations_list.clear()
+        self.ui.special_operations_list.clear()
+
+        self.add_operations_from_namespace(
+            c_oper, self.ui.core_operations_list)
+        self.add_operations_from_namespace(
+            s_oper, self.ui.special_operations_list)
+        self.logger.info("Finished refreshing operation list.")
+
+    def on_list_widget_item_clicked(self, item):
+        self._create_operation_item(item)
+
+    def keyPressEvent(self, event):
+        if event.key() == Qt.Key_Delete:
+            for pressed_op in self.pressed_operations:
+                pressed_op.remove()
+                self.move_button_index -= 1
+            self.pressed_operations.clear()
+        super().keyPressEvent(event)
+
+    def _connect_button(self, *event):
+        if len(self.pressed_ports) < 2:
+            self.logger.warning(
+                "Can't connect less than two ports. Please select more.")
+            return
+
+        for i in range(len(self.pressed_ports) - 1):
+            source = self.pressed_ports[i] if isinstance(
+                self.pressed_ports[i].port, OutputPort) else self.pressed_ports[i + 1]
+            destination = self.pressed_ports[i +
+                                             1] if source is not self.pressed_ports[i + 1] else self.pressed_ports[i]
+            if source.port.operation is destination.port.operation:
+                self.logger.warning("Can't connect to the same port")
+                continue
+
+            if type(source.port) == type(destination.port):
+                self.logger.warning(
+                    f"Can't connect port of type: {type(source.port).__name__} to port of type: {type(destination.port).__name__}.")
+                continue
+
+            self.connect_button(source, destination)
+
+        for port in self.pressed_ports:
+            port.select_port()
+
+    def connect_button(self, source, destination):
+        signal_exists = (
+            signal for signal in source.port.signals if signal.destination is destination.port)
+        self.logger.info(
+            f"Connecting: {source.operation.operation.type_name()} -> {destination.operation.operation.type_name()}.")
+        try:
+            line = Arrow(source, destination, self, signal=next(signal_exists))
+        except StopIteration:
+            line = Arrow(source, destination, self)
+
+        if line not in self.signalPortDict:
+            self.signalPortDict[line] = []
+
+        self.signalPortDict[line].append((source, destination))
+        self.scene.addItem(line)
+        self.signalList.append(line)
+
+        self.update()
+
+    def paintEvent(self, event):
+        for signal in self.signalPortDict.keys():
+            signal.moveLine()
+
+    def _select_operations(self):
+        selected = [button.widget() for button in self.scene.selectedItems()]
+        for button in selected:
+            button._toggle_button(pressed=False)
+
+        for button in self.pressed_operations:
+            if button not in selected:
+                button._toggle_button(pressed=True)
+
+        self.pressed_operations = selected
+
+    def _simulate_sfg(self):
+        for sfg, properties in self.dialog.properties.items():
+            self.logger.info(f"Simulating sfg with name: {sfg.name}.")
+            simulation = FastSimulation(
+                sfg, input_providers=properties["input_values"])
+            l_result = simulation.run_for(
+                properties["iteration_count"], save_results=properties["all_results"])
+
+            print(f"{'=' * 10} {sfg.name} {'=' * 10}")
+            pprint(
+                simulation.results if properties["all_results"] else l_result)
+            print(f"{'=' * 10} /{sfg.name} {'=' * 10}")
+
+            if properties["show_plot"]:
+                self.logger.info(
+                    f"Opening plot for sfg with name: {sfg.name}.")
+                self.logger.info(
+                    "To save the plot press 'Ctrl+S' when the plot is focused.")
+                self.plot = Plot(simulation, sfg, self)
+                self.plot.show()
+
+    def simulate_sfg(self):
+        self.dialog = SimulateSFGWindow(self)
+
+        for _, sfg in self.sfg_dict.items():
+            self.dialog.add_sfg_to_dialog(sfg)
+
+        self.dialog.show()
+
+        # Wait for input to dialog. Kinda buggy because of the separate window in the same thread.
+        self.dialog.simulate.connect(self._simulate_sfg)
+
+    def display_faq_page(self):
+        self.faq_page = FaqWindow(self)
+        self.faq_page.scroll_area.show()
+
+    def display_about_page(self):
+        self.about_page = AboutWindow(self)
+        self.about_page.show()
+
+    def display_keybinds_page(self):
+        self.keybinds_page = KeybindsWindow(self)
+        self.keybinds_page.show()
+
+
+def start_gui():
+    app = QApplication(sys.argv)
+    window = MainWindow()
+    window.show()
+    sys.exit(app.exec_())
+
+
+if __name__ == "__main__":
+    start_gui()
diff --git a/b_asic/GUI/operation_icons/abs.png b/b_asic/GUI/operation_icons/abs.png
new file mode 100644
index 0000000000000000000000000000000000000000..6573d4d96928d32a3a59641e877108ab60d38c19
Binary files /dev/null and b/b_asic/GUI/operation_icons/abs.png differ
diff --git a/b_asic/GUI/operation_icons/abs_grey.png b/b_asic/GUI/operation_icons/abs_grey.png
new file mode 100644
index 0000000000000000000000000000000000000000..4e16da3110d3497c7dab55b9cd9edf22aef4c097
Binary files /dev/null and b/b_asic/GUI/operation_icons/abs_grey.png differ
diff --git a/b_asic/GUI/operation_icons/add.png b/b_asic/GUI/operation_icons/add.png
new file mode 100644
index 0000000000000000000000000000000000000000..504e641e4642d9c03deeea9911927bbe714f053e
Binary files /dev/null and b/b_asic/GUI/operation_icons/add.png differ
diff --git a/b_asic/GUI/operation_icons/add_grey.png b/b_asic/GUI/operation_icons/add_grey.png
new file mode 100644
index 0000000000000000000000000000000000000000..a7620d2b56c8a5d06b2c04ff994eb334777008f4
Binary files /dev/null and b/b_asic/GUI/operation_icons/add_grey.png differ
diff --git a/b_asic/GUI/operation_icons/bfly.png b/b_asic/GUI/operation_icons/bfly.png
new file mode 100644
index 0000000000000000000000000000000000000000..9948a964d353e7325c696ae7f12e1ded09cdc13f
Binary files /dev/null and b/b_asic/GUI/operation_icons/bfly.png differ
diff --git a/b_asic/GUI/operation_icons/bfly_grey.png b/b_asic/GUI/operation_icons/bfly_grey.png
new file mode 100644
index 0000000000000000000000000000000000000000..cc282efe67637dda8b5e76b0f4c35015b19ddd93
Binary files /dev/null and b/b_asic/GUI/operation_icons/bfly_grey.png differ
diff --git a/b_asic/GUI/operation_icons/c.png b/b_asic/GUI/operation_icons/c.png
new file mode 100644
index 0000000000000000000000000000000000000000..0068adae8130f7b384f7fd70277fb8f87d9dbf94
Binary files /dev/null and b/b_asic/GUI/operation_icons/c.png differ
diff --git a/b_asic/GUI/operation_icons/c_grey.png b/b_asic/GUI/operation_icons/c_grey.png
new file mode 100644
index 0000000000000000000000000000000000000000..d7d3e585e21e70aa8b89b85008e5306184ccff8c
Binary files /dev/null and b/b_asic/GUI/operation_icons/c_grey.png differ
diff --git a/b_asic/GUI/operation_icons/cmul.png b/b_asic/GUI/operation_icons/cmul.png
new file mode 100644
index 0000000000000000000000000000000000000000..7e7ff82b3aa577886da6686f62df936e8aa3572e
Binary files /dev/null and b/b_asic/GUI/operation_icons/cmul.png differ
diff --git a/b_asic/GUI/operation_icons/cmul_grey.png b/b_asic/GUI/operation_icons/cmul_grey.png
new file mode 100644
index 0000000000000000000000000000000000000000..8fe92d2606b8bc1c2bce98ed45cccb6236e36476
Binary files /dev/null and b/b_asic/GUI/operation_icons/cmul_grey.png differ
diff --git a/b_asic/GUI/operation_icons/conj.png b/b_asic/GUI/operation_icons/conj.png
new file mode 100644
index 0000000000000000000000000000000000000000..c74c9de7f45c16b972ddc072e5e0203dcb902f1b
Binary files /dev/null and b/b_asic/GUI/operation_icons/conj.png differ
diff --git a/b_asic/GUI/operation_icons/conj_grey.png b/b_asic/GUI/operation_icons/conj_grey.png
new file mode 100644
index 0000000000000000000000000000000000000000..33f1e60e686cb711644a875341c34a12b8dfe4c9
Binary files /dev/null and b/b_asic/GUI/operation_icons/conj_grey.png differ
diff --git a/b_asic/GUI/operation_icons/custom_operation.png b/b_asic/GUI/operation_icons/custom_operation.png
new file mode 100644
index 0000000000000000000000000000000000000000..a598abaaf05934222c457fc8b141431ca04f91be
Binary files /dev/null and b/b_asic/GUI/operation_icons/custom_operation.png differ
diff --git a/b_asic/GUI/operation_icons/custom_operation_grey.png b/b_asic/GUI/operation_icons/custom_operation_grey.png
new file mode 100644
index 0000000000000000000000000000000000000000..a1d72e3b7f67fb78acccf8f5fb417be62cbc2b2a
Binary files /dev/null and b/b_asic/GUI/operation_icons/custom_operation_grey.png differ
diff --git a/b_asic/GUI/operation_icons/div.png b/b_asic/GUI/operation_icons/div.png
new file mode 100644
index 0000000000000000000000000000000000000000..d7bf8908ed0acae344dc04adf6a96ae8448bb9d4
Binary files /dev/null and b/b_asic/GUI/operation_icons/div.png differ
diff --git a/b_asic/GUI/operation_icons/div_grey.png b/b_asic/GUI/operation_icons/div_grey.png
new file mode 100644
index 0000000000000000000000000000000000000000..de3a82c369dde1b6a28ea93e5362e6eb8879fe68
Binary files /dev/null and b/b_asic/GUI/operation_icons/div_grey.png differ
diff --git a/b_asic/GUI/operation_icons/in.png b/b_asic/GUI/operation_icons/in.png
new file mode 100644
index 0000000000000000000000000000000000000000..ebfd1a23e5723496e69f02bebc959d68e3c2f986
Binary files /dev/null and b/b_asic/GUI/operation_icons/in.png differ
diff --git a/b_asic/GUI/operation_icons/in_grey.png b/b_asic/GUI/operation_icons/in_grey.png
new file mode 100644
index 0000000000000000000000000000000000000000..07d3362f93039ca65966bc04d0ea481840072559
Binary files /dev/null and b/b_asic/GUI/operation_icons/in_grey.png differ
diff --git a/b_asic/GUI/operation_icons/max.png b/b_asic/GUI/operation_icons/max.png
new file mode 100644
index 0000000000000000000000000000000000000000..1f759155f632e090a5dea49644f8dbc5dff7b4f4
Binary files /dev/null and b/b_asic/GUI/operation_icons/max.png differ
diff --git a/b_asic/GUI/operation_icons/max_grey.png b/b_asic/GUI/operation_icons/max_grey.png
new file mode 100644
index 0000000000000000000000000000000000000000..8179fb2e812cc617a012a1fb3d338e1c99e07aa7
Binary files /dev/null and b/b_asic/GUI/operation_icons/max_grey.png differ
diff --git a/b_asic/GUI/operation_icons/min.png b/b_asic/GUI/operation_icons/min.png
new file mode 100644
index 0000000000000000000000000000000000000000..5365002a74b91d91e40c95091ab8c052cfe3f3c5
Binary files /dev/null and b/b_asic/GUI/operation_icons/min.png differ
diff --git a/b_asic/GUI/operation_icons/min_grey.png b/b_asic/GUI/operation_icons/min_grey.png
new file mode 100644
index 0000000000000000000000000000000000000000..6978cd91288a9d5705903efb017e2dc3b42b1af1
Binary files /dev/null and b/b_asic/GUI/operation_icons/min_grey.png differ
diff --git a/b_asic/GUI/operation_icons/mul.png b/b_asic/GUI/operation_icons/mul.png
new file mode 100644
index 0000000000000000000000000000000000000000..2042dd16781e64ea97ed5323b79f4f310f760009
Binary files /dev/null and b/b_asic/GUI/operation_icons/mul.png differ
diff --git a/b_asic/GUI/operation_icons/mul_grey.png b/b_asic/GUI/operation_icons/mul_grey.png
new file mode 100644
index 0000000000000000000000000000000000000000..00e2304b634e02810d6a17aa2850c9afe4922eb9
Binary files /dev/null and b/b_asic/GUI/operation_icons/mul_grey.png differ
diff --git a/b_asic/GUI/operation_icons/out.png b/b_asic/GUI/operation_icons/out.png
new file mode 100644
index 0000000000000000000000000000000000000000..e7da51bbe3640b03b9f7d89f9dd90543c3022273
Binary files /dev/null and b/b_asic/GUI/operation_icons/out.png differ
diff --git a/b_asic/GUI/operation_icons/out_grey.png b/b_asic/GUI/operation_icons/out_grey.png
new file mode 100644
index 0000000000000000000000000000000000000000..2cde317beecf019db5c4eac2a35baa8ef8e99f5e
Binary files /dev/null and b/b_asic/GUI/operation_icons/out_grey.png differ
diff --git a/b_asic/GUI/operation_icons/sqrt.png b/b_asic/GUI/operation_icons/sqrt.png
new file mode 100644
index 0000000000000000000000000000000000000000..8160862b675680bb5fc2f31a0a5f870b73352bbb
Binary files /dev/null and b/b_asic/GUI/operation_icons/sqrt.png differ
diff --git a/b_asic/GUI/operation_icons/sqrt_grey.png b/b_asic/GUI/operation_icons/sqrt_grey.png
new file mode 100644
index 0000000000000000000000000000000000000000..4353217de9c38d665f1364a7f7c501c444232abc
Binary files /dev/null and b/b_asic/GUI/operation_icons/sqrt_grey.png differ
diff --git a/b_asic/GUI/operation_icons/sub.png b/b_asic/GUI/operation_icons/sub.png
new file mode 100644
index 0000000000000000000000000000000000000000..73db57daf9984fd1c1eb0775a1e0535b786396a8
Binary files /dev/null and b/b_asic/GUI/operation_icons/sub.png differ
diff --git a/b_asic/GUI/operation_icons/sub_grey.png b/b_asic/GUI/operation_icons/sub_grey.png
new file mode 100644
index 0000000000000000000000000000000000000000..8b32557a56f08e27adcd028f080d517f6ce5bdf2
Binary files /dev/null and b/b_asic/GUI/operation_icons/sub_grey.png differ
diff --git a/b_asic/GUI/operation_icons/t.png b/b_asic/GUI/operation_icons/t.png
new file mode 100644
index 0000000000000000000000000000000000000000..f294072fc6d3b3567d8252aee881d035aa4913eb
Binary files /dev/null and b/b_asic/GUI/operation_icons/t.png differ
diff --git a/b_asic/GUI/operation_icons/t_grey.png b/b_asic/GUI/operation_icons/t_grey.png
new file mode 100644
index 0000000000000000000000000000000000000000..88af5760169b161fdd7aa5dce7c61cdb13b69231
Binary files /dev/null and b/b_asic/GUI/operation_icons/t_grey.png differ
diff --git a/b_asic/GUI/port_button.py b/b_asic/GUI/port_button.py
new file mode 100644
index 0000000000000000000000000000000000000000..a856b618578c6e271c8717a7e9d6e2bb78a62b67
--- /dev/null
+++ b/b_asic/GUI/port_button.py
@@ -0,0 +1,59 @@
+
+import sys
+
+from PySide2.QtWidgets import QPushButton, QMenu
+from PySide2.QtCore import Qt, Signal
+
+
+class PortButton(QPushButton):
+    connectionRequested = Signal(QPushButton)
+    moved = Signal()
+    def __init__(self, name, operation, port, window, parent=None):
+        super(PortButton, self).__init__(name, operation, parent)
+        self.pressed = False
+        self._window = window
+        self.port = port
+        self.operation = operation
+        self.clicked = 0
+        self._m_drag = False
+        self._m_press = False
+
+        self.setStyleSheet("background-color: white")
+        self.connectionRequested.connect(self._window._connect_button)
+
+    def contextMenuEvent(self, event):
+        menu = QMenu()
+        menu.addAction("Connect", lambda: self.connectionRequested.emit(self))
+        menu.exec_(self.cursor().pos())
+
+    def mousePressEvent(self, event):
+        if event.button() == Qt.LeftButton:
+            self.select_port(event.modifiers())
+
+        super(PortButton, self).mousePressEvent(event)
+
+    def mouseReleaseEvent(self, event):
+        super(PortButton, self).mouseReleaseEvent(event)
+
+    def _toggle_port(self, pressed=False):
+        self.pressed = not pressed
+        self.setStyleSheet(f"background-color: {'white' if not self.pressed else 'grey'}")
+
+    def select_port(self, modifiers=None):
+        if modifiers != Qt.ControlModifier:
+            for port in self._window.pressed_ports:
+                port._toggle_port(port.pressed)
+
+            self._toggle_port(self.pressed)
+            self._window.pressed_ports = [self]
+
+        else:
+            self._toggle_port(self.pressed)
+            if self in self._window.pressed_ports:
+                self._window.pressed_ports.remove(self)
+            else:
+                self._window.pressed_ports.append(self)
+
+        for signal in self._window.signalList:
+            signal.update()
+
diff --git a/b_asic/GUI/properties_window.py b/b_asic/GUI/properties_window.py
new file mode 100644
index 0000000000000000000000000000000000000000..3c690517d446b9131df63d06413698e25e13820e
--- /dev/null
+++ b/b_asic/GUI/properties_window.py
@@ -0,0 +1,146 @@
+from PySide2.QtWidgets import QDialog, QLineEdit, QPushButton, QVBoxLayout, QHBoxLayout,\
+QLabel, QCheckBox, QGridLayout
+from PySide2.QtCore import Qt
+from PySide2.QtGui import QDoubleValidator
+
+
+class PropertiesWindow(QDialog):
+    def __init__(self, operation, main_window):
+        super(PropertiesWindow, self).__init__()
+        self.operation = operation
+        self._window = main_window
+        self.setWindowFlags(Qt.WindowTitleHint | Qt.WindowCloseButtonHint)
+        self.setWindowTitle("Properties")
+
+        self.name_layout = QHBoxLayout()
+        self.name_layout.setSpacing(50)
+        self.name_label = QLabel("Name:")
+        self.edit_name = QLineEdit(self.operation.operation_path_name)
+        self.name_layout.addWidget(self.name_label)
+        self.name_layout.addWidget(self.edit_name)
+        self.latency_fields = dict()
+
+        self.vertical_layout = QVBoxLayout()
+        self.vertical_layout.addLayout(self.name_layout)
+
+        if hasattr(self.operation.operation, "value") or hasattr(self.operation.operation, "initial_value"):
+            self.constant_layout = QHBoxLayout()
+            self.constant_layout.setSpacing(50)
+            self.constant_value = QLabel("Value:")
+            if hasattr(self.operation.operation, "value"):
+                self.edit_constant = QLineEdit(str(self.operation.operation.value))
+            else:
+                self.edit_constant = QLineEdit(str(self.operation.operation.initial_value))
+
+            self.only_accept_float = QDoubleValidator()
+            self.edit_constant.setValidator(self.only_accept_float)
+            self.constant_layout.addWidget(self.constant_value)
+            self.constant_layout.addWidget(self.edit_constant)
+            self.vertical_layout.addLayout(self.constant_layout)
+
+        self.show_name_layout = QHBoxLayout()
+        self.check_show_name = QCheckBox("Show name?")
+        if self.operation.is_show_name:
+            self.check_show_name.setChecked(1)
+        else:
+            self.check_show_name.setChecked(0)
+        self.check_show_name.setLayoutDirection(Qt.RightToLeft)
+        self.check_show_name.setStyleSheet("spacing: 170px")
+        self.show_name_layout.addWidget(self.check_show_name)
+        self.vertical_layout.addLayout(self.show_name_layout)
+
+        if self.operation.operation.input_count > 0:
+            self.latency_layout = QHBoxLayout()
+            self.latency_label = QLabel("Set Latency For Input Ports (-1 for None):")
+            self.latency_layout.addWidget(self.latency_label)
+            self.vertical_layout.addLayout(self.latency_layout)
+
+            input_grid = QGridLayout()
+            x, y = 0, 0
+            for i in range(self.operation.operation.input_count):
+                input_layout = QHBoxLayout()
+                input_layout.addStretch()
+                if i % 2 == 0 and i > 0:
+                    x += 1
+                    y = 0
+
+                input_label = QLabel("in" + str(i))
+                input_layout.addWidget(input_label)
+                input_value = QLineEdit()
+                try:
+                    input_value.setPlaceholderText(str(self.operation.operation.latency))
+                except ValueError:
+                    input_value.setPlaceholderText("-1")
+                int_valid = QDoubleValidator()
+                int_valid.setBottom(-1)
+                input_value.setValidator(int_valid)
+                input_value.setFixedWidth(50)
+                self.latency_fields["in" + str(i)] = input_value
+                input_layout.addWidget(input_value)
+                input_layout.addStretch()
+                input_layout.setSpacing(10)
+                input_grid.addLayout(input_layout, x, y)
+                y += 1
+
+            self.vertical_layout.addLayout(input_grid)
+
+        if self.operation.operation.output_count > 0:
+            self.latency_layout = QHBoxLayout()
+            self.latency_label = QLabel("Set Latency For Output Ports (-1 for None):")
+            self.latency_layout.addWidget(self.latency_label)
+            self.vertical_layout.addLayout(self.latency_layout)
+
+            input_grid = QGridLayout()
+            x, y = 0, 0
+            for i in range(self.operation.operation.output_count):
+                input_layout = QHBoxLayout()
+                input_layout.addStretch()
+                if i % 2 == 0 and i > 0:
+                    x += 1
+                    y = 0
+
+                input_label = QLabel("out" + str(i))
+                input_layout.addWidget(input_label)
+                input_value = QLineEdit()
+                try:
+                    input_value.setPlaceholderText(str(self.operation.operation.latency))
+                except ValueError:
+                    input_value.setPlaceholderText("-1")
+                int_valid = QDoubleValidator()
+                int_valid.setBottom(-1)
+                input_value.setValidator(int_valid)
+                input_value.setFixedWidth(50)
+                self.latency_fields["out" + str(i)] = input_value
+                input_layout.addWidget(input_value)
+                input_layout.addStretch()
+                input_layout.setSpacing(10)
+                input_grid.addLayout(input_layout, x, y)
+                y += 1
+
+            self.vertical_layout.addLayout(input_grid)
+
+        self.ok = QPushButton("OK")
+        self.ok.clicked.connect(self.save_properties)
+        self.vertical_layout.addWidget(self.ok)
+        self.setLayout(self.vertical_layout)
+
+    def save_properties(self):
+        self._window.logger.info(f"Saving properties of operation: {self.operation.name}.")
+        self.operation.name = self.edit_name.text()
+        self.operation.operation.name = self.edit_name.text()
+        self.operation.label.setPlainText(self.operation.name)
+        if hasattr(self.operation.operation, "value"):
+            self.operation.operation.value = float(self.edit_constant.text().replace(",", "."))
+        elif hasattr(self.operation.operation, "initial_value"):
+            self.operation.operation.initial_value = float(self.edit_constant.text().replace(",", "."))
+
+        if self.check_show_name.isChecked():
+            self.operation.label.setOpacity(1)
+            self.operation.is_show_name = True
+        else:
+            self.operation.label.setOpacity(0)
+            self.operation.is_show_name = False
+
+        self.operation.operation.set_latency_offsets({port: float(self.latency_fields[port].text().replace(",", ".")) if self.latency_fields[port].text() and float(self.latency_fields[port].text().replace(",", ".")) > 0 else None for port in self.latency_fields})
+
+        self.reject()
\ No newline at end of file
diff --git a/b_asic/GUI/select_sfg_window.py b/b_asic/GUI/select_sfg_window.py
new file mode 100644
index 0000000000000000000000000000000000000000..1b70c2c27f066f8ab28808673d347eec717f1e5c
--- /dev/null
+++ b/b_asic/GUI/select_sfg_window.py
@@ -0,0 +1,39 @@
+from PySide2.QtWidgets import QDialog, QLineEdit, QPushButton, QVBoxLayout, QHBoxLayout,\
+QLabel, QCheckBox, QSpinBox, QGroupBox, QFrame, QFormLayout, QGridLayout, QSizePolicy, QFileDialog, QShortcut, QComboBox
+from PySide2.QtCore import Qt, Signal
+from PySide2.QtGui import QIntValidator, QKeySequence
+
+from matplotlib.backends import qt_compat
+from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
+from matplotlib.figure import Figure
+
+
+class SelectSFGWindow(QDialog):
+    ok = Signal()
+
+    def __init__(self, window):
+        super(SelectSFGWindow, self).__init__()
+        self._window = window
+        self.setWindowFlags(Qt.WindowTitleHint | Qt.WindowCloseButtonHint)
+        self.setWindowTitle("Select SFG")
+
+        self.dialog_layout = QVBoxLayout()
+        self.ok_btn = QPushButton("Ok")
+        self.ok_btn.clicked.connect(self.save_properties)
+        self.dialog_layout.addWidget(self.ok_btn)
+        self.combo_box = QComboBox()
+
+        self.sfg = None
+        self.setLayout(self.dialog_layout)
+        self.add_sfgs_to_layout()
+
+    def add_sfgs_to_layout(self):
+        for sfg in self._window.sfg_dict:
+            self.combo_box.addItem(sfg)
+
+        self.dialog_layout.addWidget(self.combo_box)
+
+    def save_properties(self):
+        self.sfg = self._window.sfg_dict[self.combo_box.currentText()]
+        self.accept()
+        self.ok.emit()
diff --git a/b_asic/GUI/show_pc_window.py b/b_asic/GUI/show_pc_window.py
new file mode 100644
index 0000000000000000000000000000000000000000..1cc879397f726c343350ea33483e5850404b851b
--- /dev/null
+++ b/b_asic/GUI/show_pc_window.py
@@ -0,0 +1,49 @@
+from b_asic.signal_flow_graph import SFG
+
+from PySide2.QtWidgets import QDialog, QPushButton, QVBoxLayout, QCheckBox,\
+QFrame, QFormLayout
+from PySide2.QtCore import Qt, Signal
+
+
+class ShowPCWindow(QDialog):
+    pc = Signal()
+
+    def __init__(self, window):
+        super(ShowPCWindow, self).__init__()
+        self._window = window
+        self.check_box_dict = dict()
+        self.setWindowFlags(Qt.WindowTitleHint | Qt.WindowCloseButtonHint)
+        self.setWindowTitle("Show PC")
+
+        self.dialog_layout = QVBoxLayout()
+        self.pc_btn = QPushButton("Show PC")
+        self.pc_btn.clicked.connect(self.show_precedence_graph)
+        self.dialog_layout.addWidget(self.pc_btn)
+        self.setLayout(self.dialog_layout)
+
+    def add_sfg_to_dialog(self):
+        self.sfg_layout = QVBoxLayout()
+        self.options_layout = QFormLayout()
+
+        for sfg in self._window.sfg_dict:
+            check_box = QCheckBox()
+            self.options_layout.addRow(sfg, check_box)
+            self.check_box_dict[check_box] = sfg
+
+        self.sfg_layout.addLayout(self.options_layout)
+
+        frame = QFrame()
+        frame.setFrameShape(QFrame.HLine)
+        frame.setFrameShadow(QFrame.Sunken)
+        self.dialog_layout.addWidget(frame)
+
+        self.dialog_layout.addLayout(self.sfg_layout)
+
+    def show_precedence_graph(self):
+        for check_box, sfg in self.check_box_dict.items():
+            if check_box.isChecked():
+                self._window.logger.info(f"Creating a precedence chart from sfg with name: {sfg}.")
+                self._window.sfg_dict[sfg].show_precedence_graph()
+
+        self.accept()
+        self.pc.emit()
diff --git a/b_asic/GUI/simulate_sfg_window.py b/b_asic/GUI/simulate_sfg_window.py
new file mode 100644
index 0000000000000000000000000000000000000000..f787e1ce25c0881ffdc5fa0c238ead0a971c3d61
--- /dev/null
+++ b/b_asic/GUI/simulate_sfg_window.py
@@ -0,0 +1,175 @@
+from PySide2.QtWidgets import QDialog, QLineEdit, QPushButton, QVBoxLayout, QHBoxLayout,\
+QLabel, QCheckBox, QSpinBox, QGroupBox, QFrame, QFormLayout, QGridLayout, QSizePolicy, QFileDialog, QShortcut
+from PySide2.QtCore import Qt, Signal
+from PySide2.QtGui import QDoubleValidator, QKeySequence
+
+from matplotlib.backends import qt_compat
+from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
+from matplotlib.figure import Figure
+
+
+class SimulateSFGWindow(QDialog):
+    simulate = Signal()
+
+    def __init__(self, window):
+        super(SimulateSFGWindow, self).__init__()
+        self._window = window
+        self.properties = dict()
+        self.sfg_to_layout = dict()
+        self.input_fields = dict()
+        self.setWindowFlags(Qt.WindowTitleHint | Qt.WindowCloseButtonHint)
+        self.setWindowTitle("Simulate SFG")
+
+        self.dialog_layout = QVBoxLayout()
+        self.simulate_btn = QPushButton("Simulate")
+        self.simulate_btn.clicked.connect(self.save_properties)
+        self.dialog_layout.addWidget(self.simulate_btn)
+        self.setLayout(self.dialog_layout)
+
+    def add_sfg_to_dialog(self, sfg):
+        sfg_layout = QVBoxLayout()
+        options_layout = QFormLayout()
+
+        name_label = QLabel(f"{sfg.name}")
+        sfg_layout.addWidget(name_label)
+
+        spin_box = QSpinBox()
+        spin_box.setRange(0, 2147483647)
+        options_layout.addRow("Iteration Count: ", spin_box)
+
+        check_box_plot = QCheckBox()
+        options_layout.addRow("Plot Results: ", check_box_plot)
+
+        check_box_all = QCheckBox()
+        options_layout.addRow("Get All Results: ", check_box_all)
+
+        sfg_layout.addLayout(options_layout)
+
+        self.input_fields[sfg] = {
+            "iteration_count": spin_box,
+            "show_plot": check_box_plot,
+            "all_results": check_box_all,
+            "input_values": []
+        }
+
+        if sfg.input_count > 0:
+            input_label = QHBoxLayout()
+            input_label = QLabel("Input Values:")
+            options_layout.addRow(input_label)
+
+            input_grid = QGridLayout()
+            x, y = 0, 0
+            for i in range(sfg.input_count):
+                input_layout = QHBoxLayout()
+                input_layout.addStretch()
+                if i % 2 == 0 and i > 0:
+                    x += 1
+                    y = 0
+
+                input_label = QLabel("in" + str(i))
+                input_layout.addWidget(input_label)
+                input_value = QLineEdit()
+                input_value.setPlaceholderText("e.g 0, 0, 0")
+                input_value.setFixedWidth(100)
+                input_layout.addWidget(input_value)
+                input_layout.addStretch()
+                input_layout.setSpacing(10)
+                input_grid.addLayout(input_layout, x, y)
+
+                self.input_fields[sfg]["input_values"].append(input_value)
+                y += 1
+
+            sfg_layout.addLayout(input_grid)
+
+        frame = QFrame()
+        frame.setFrameShape(QFrame.HLine)
+        frame.setFrameShadow(QFrame.Sunken)
+        self.dialog_layout.addWidget(frame)
+
+        self.sfg_to_layout[sfg] = sfg_layout
+        self.dialog_layout.addLayout(sfg_layout)
+
+    def parse_input_values(self, input_values):
+        _input_values = []
+        for _list in list(input_values):
+            _list_values = []
+            for val in _list:
+                val = val.strip()
+                try:
+                    if not val:
+                        val = 0
+
+                    _list_values.append(complex(val))
+                except ValueError:
+                    self._window.logger.warning(f"Skipping value: {val}, not a digit.")
+                    continue
+
+            _input_values.append(_list_values)
+
+        return _input_values
+
+    def save_properties(self):
+        for sfg, properties in self.input_fields.items():
+            input_values = self.parse_input_values(widget.text().split(",") if widget.text() else [0] for widget in self.input_fields[sfg]["input_values"])
+            if max(len(list_) for list_ in input_values) != min(len(list_) for list_ in input_values):
+                self._window.logger.error(f"Minimum length of input lists are not equal to maximum length of input lists: {max(len(list_) for list_ in input_values)} != {min(len(list_) for list_ in input_values)}.")
+            elif self.input_fields[sfg]["iteration_count"].value() > min(len(list_) for list_ in input_values):
+                self._window.logger.error(f"Minimum length of input lists are less than the iteration count: {self.input_fields[sfg]['iteration_count'].value()} > {min(len(list_) for list_ in input_values)}.")
+            else:
+                self.properties[sfg] = {
+                    "iteration_count": self.input_fields[sfg]["iteration_count"].value(),
+                    "show_plot": self.input_fields[sfg]["show_plot"].isChecked(),
+                    "all_results": self.input_fields[sfg]["all_results"].isChecked(),
+                    "input_values": input_values
+                }
+
+                # If we plot we should also print the entire data, since you can't really interact with the graph.
+                if self.properties[sfg]["show_plot"]:
+                    self.properties[sfg]["all_results"] = True
+
+                continue
+
+            self._window.logger.info(f"Skipping simulation of sfg with name: {sfg.name}, due to previous errors.")
+
+        self.accept()
+        self.simulate.emit()
+
+
+class Plot(FigureCanvas):
+    def __init__(self, simulation, sfg, window, parent=None, width=5, height=4, dpi=100):
+        self.simulation = simulation
+        self.sfg = sfg
+        self.dpi = dpi
+        self._window = window
+
+        fig = Figure(figsize=(width, height), dpi=dpi)
+        fig.suptitle(sfg.name, fontsize=20)
+        self.axes = fig.add_subplot(111)
+
+        FigureCanvas.__init__(self, fig)
+        self.setParent(parent)
+
+        FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding)
+        FigureCanvas.updateGeometry(self)
+        self.save_figure = QShortcut(QKeySequence("Ctrl+S"), self)
+        self.save_figure.activated.connect(self._save_plot_figure)
+        self._plot_values_sfg()
+
+    def _save_plot_figure(self):
+        self._window.logger.info(f"Saving plot of figure: {self.sfg.name}.")
+        file_choices = "PNG (*.png)|*.png"
+        path, ext = QFileDialog.getSaveFileName(self, "Save file", "", file_choices)
+        path = path.encode("utf-8")
+        if not path[-4:] == file_choices[-4:].encode("utf-8"):
+            path += file_choices[-4:].encode("utf-8")
+
+        if path:
+            self.print_figure(path.decode(), dpi=self.dpi)
+            self._window.logger.info(f"Saved plot: {self.sfg.name} to path: {path}.")
+
+    def _plot_values_sfg(self):
+        x_axis = list(range(len(self.simulation.results["0"])))
+        for _output in range(self.sfg.output_count):
+            y_axis = self.simulation.results[str(_output)]
+
+            self.axes.plot(x_axis, y_axis)
diff --git a/b_asic/GUI/utils.py b/b_asic/GUI/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..5234c6548bbbc66eb224fb9d3d3835d67a099213
--- /dev/null
+++ b/b_asic/GUI/utils.py
@@ -0,0 +1,20 @@
+from PySide2.QtWidgets import QErrorMessage
+from traceback import format_exc
+
+def handle_error(fn):
+    def wrapper(self, *args, **kwargs):
+        try:
+            return fn(self, *args, **kwargs)
+        except Exception as e:
+            self._window.logger.error(f"Unexpected error: {format_exc()}")
+            QErrorMessage(self._window).showMessage(f"Unexpected error: {format_exc()}")
+
+    return wrapper
+
+def decorate_class(decorator):
+    def decorate(cls):
+        for attr in cls.__dict__:
+            if callable(getattr(cls, attr)):
+                setattr(cls, attr, decorator(getattr(cls, attr)))
+        return cls
+    return decorate
\ No newline at end of file
diff --git a/b_asic/__init__.py b/b_asic/__init__.py
index 7e40ad52cdc51da3e1d91964ad55cfc90e12a34a..e69f04987f4ff2921e57d7f0274e1c8c1e5c3ebd 100644
--- a/b_asic/__init__.py
+++ b/b_asic/__init__.py
@@ -1,14 +1,19 @@
+"""B-ASIC - Better ASIC Toolbox.
+ASIC toolbox that simplifies circuit design and optimization.
 """
-Better ASIC Toolbox.
-TODO: More info.
-"""
+# Extension module (C++).
+# NOTE: If this import gives an error,
+# make sure the C++ module has been compiled and installed properly.
+# See the included README.md for more information on how to build/install.
+from _b_asic import *
+# Python modules.
 from b_asic.core_operations import *
 from b_asic.graph_component import *
-from b_asic.graph_id import *
 from b_asic.operation import *
-from b_asic.precedence_chart import *
 from b_asic.port import *
-from b_asic.schema import *
 from b_asic.signal_flow_graph import *
 from b_asic.signal import *
 from b_asic.simulation import *
+from b_asic.special_operations import *
+from b_asic.save_load_structure import *
+from b_asic.schema import *
diff --git a/b_asic/core_operations.py b/b_asic/core_operations.py
index 8902b169c07e600a843e526d44452fb9386ff7a9..c822fe8f4843c26f5dfabc33a48f3cbe92d74eef 100644
--- a/b_asic/core_operations.py
+++ b/b_asic/core_operations.py
@@ -1,337 +1,315 @@
-"""@package docstring
-B-ASIC Core Operations Module.
-TODO: More info.
+"""B-ASIC Core Operations Module.
+
+Contains some of the most commonly used mathematical operations.
 """
 
 from numbers import Number
-from typing import Any
+from typing import Optional, Dict
 from numpy import conjugate, sqrt, abs as np_abs
-from b_asic.port import InputPort, OutputPort
-from b_asic.graph_id import GraphIDType
+
+from b_asic.port import SignalSourceProvider, InputPort, OutputPort
 from b_asic.operation import AbstractOperation
 from b_asic.graph_component import Name, TypeName
 
 
-class Input(AbstractOperation):
-    """Input operation.
-    TODO: More info.
-    """
-
-    # TODO: Implement all functions.
-
-    @property
-    def type_name(self) -> TypeName:
-        return "in"
-
-
 class Constant(AbstractOperation):
     """Constant value operation.
-    TODO: More info.
+
+    Gives a specified value that remains constant for every iteration.
+
+    output(0): self.param("value")
     """
 
     def __init__(self, value: Number = 0, name: Name = ""):
-        super().__init__(name)
+        """Construct a Constant operation with the given value."""
+        super().__init__(input_count=0, output_count=1, name=name)
+        self.set_param("value", value)
 
-        self._output_ports = [OutputPort(0, self)]
-        self._parameters["value"] = value
+    @classmethod
+    def type_name(cls) -> TypeName:
+        return "c"
 
     def evaluate(self):
         return self.param("value")
 
     @property
-    def type_name(self) -> TypeName:
-        return "c"
+    def value(self) -> Number:
+        """Get the constant value of this operation."""
+        return self.param("value")
+
+    @value.setter
+    def value(self, value: Number) -> None:
+        """Set the constant value of this operation."""
+        return self.set_param("value", value)
 
 
 class Addition(AbstractOperation):
     """Binary addition operation.
-    TODO: More info.
-    """
 
-    def __init__(self, source1: OutputPort = None, source2: OutputPort = None, name: Name = ""):
-        super().__init__(name)
+    Gives the result of adding two inputs.
+
+    output(0): input(0) + input(1)
+    """
 
-        self._input_ports = [InputPort(0, self), InputPort(1, self)]
-        self._output_ports = [OutputPort(0, self)]
+    def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = "", latency: Optional[int] = None, latency_offsets: Optional[Dict[str, int]] = None):
+        """Construct an Addition operation."""
+        super().__init__(input_count=2, output_count=1, name=name, input_sources=[src0, src1],
+                         latency=latency, latency_offsets=latency_offsets)
 
-        if source1 is not None:
-            self._input_ports[0].connect(source1)
-        if source2 is not None:
-            self._input_ports[1].connect(source2)
+    @classmethod
+    def type_name(cls) -> TypeName:
+        return "add"
 
     def evaluate(self, a, b):
         return a + b
 
-    @property
-    def type_name(self) -> TypeName:
-        return "add"
-
 
 class Subtraction(AbstractOperation):
     """Binary subtraction operation.
-    TODO: More info.
+
+    Gives the result of subtracting the second input from the first one.
+
+    output(0): input(0) - input(1)
     """
 
-    def __init__(self, source1: OutputPort = None, source2: OutputPort = None, name: Name = ""):
-        super().__init__(name)
-        self._input_ports = [InputPort(0, self), InputPort(1, self)]
-        self._output_ports = [OutputPort(0, self)]
+    def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = "", latency: Optional[int] = None, latency_offsets: Optional[Dict[str, int]] = None):
+        """Construct a Subtraction operation."""
+        super().__init__(input_count=2, output_count=1, name=name, input_sources=[src0, src1],
+                         latency=latency, latency_offsets=latency_offsets)
 
-        if source1 is not None:
-            self._input_ports[0].connect(source1)
-        if source2 is not None:
-            self._input_ports[1].connect(source2)
+    @classmethod
+    def type_name(cls) -> TypeName:
+        return "sub"
 
     def evaluate(self, a, b):
         return a - b
 
-    @property
-    def type_name(self) -> TypeName:
-        return "sub"
-
 
 class Multiplication(AbstractOperation):
     """Binary multiplication operation.
-    TODO: More info.
+
+    Gives the result of multiplying two inputs.
+
+    output(0): input(0) * input(1)
     """
 
-    def __init__(self, source1: OutputPort = None, source2: OutputPort = None, name: Name = ""):
-        super().__init__(name)
-        self._input_ports = [InputPort(0, self), InputPort(1, self)]
-        self._output_ports = [OutputPort(0, self)]
+    def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = "", latency: Optional[int] = None, latency_offsets: Optional[Dict[str, int]] = None):
+        """Construct a Multiplication operation."""
+        super().__init__(input_count=2, output_count=1, name=name, input_sources=[src0, src1],
+                         latency=latency, latency_offsets=latency_offsets)
 
-        if source1 is not None:
-            self._input_ports[0].connect(source1)
-        if source2 is not None:
-            self._input_ports[1].connect(source2)
+    @classmethod
+    def type_name(cls) -> TypeName:
+        return "mul"
 
     def evaluate(self, a, b):
         return a * b
 
-    @property
-    def type_name(self) -> TypeName:
-        return "mul"
-
 
 class Division(AbstractOperation):
     """Binary division operation.
-    TODO: More info.
-    """
 
-    def __init__(self, source1: OutputPort = None, source2: OutputPort = None, name: Name = ""):
-        super().__init__(name)
-        self._input_ports = [InputPort(0, self), InputPort(1, self)]
-        self._output_ports = [OutputPort(0, self)]
+    Gives the result of dividing the first input by the second one.
 
-        if source1 is not None:
-            self._input_ports[0].connect(source1)
-        if source2 is not None:
-            self._input_ports[1].connect(source2)
+    output(0): input(0) / input(1)
+    """
 
-    def evaluate(self, a, b):
-        return a / b
+    def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = "", latency: Optional[int] = None, latency_offsets: Optional[Dict[str, int]] = None):
+        """Construct a Division operation."""
+        super().__init__(input_count=2, output_count=1, name=name, input_sources=[src0, src1],
+                         latency=latency, latency_offsets=latency_offsets)
 
-    @property
-    def type_name(self) -> TypeName:
+    @classmethod
+    def type_name(cls) -> TypeName:
         return "div"
 
+    def evaluate(self, a, b):
+        return a / b
 
-class SquareRoot(AbstractOperation):
-    """Unary square root operation.
-    TODO: More info.
-    """
-
-    def __init__(self, source1: OutputPort = None, name: Name = ""):
-        super().__init__(name)
-        self._input_ports = [InputPort(0, self)]
-        self._output_ports = [OutputPort(0, self)]
-
-        if source1 is not None:
-            self._input_ports[0].connect(source1)
 
-    def evaluate(self, a):
-        return sqrt((complex)(a))
-
-    @property
-    def type_name(self) -> TypeName:
-        return "sqrt"
+class Min(AbstractOperation):
+    """Binary min operation.
 
+    Gives the minimum value of two inputs.
+    NOTE: Non-real numbers are not supported.
 
-class ComplexConjugate(AbstractOperation):
-    """Unary complex conjugate operation.
-    TODO: More info.
+    output(0): min(input(0), input(1))
     """
 
-    def __init__(self, source1: OutputPort = None, name: Name = ""):
-        super().__init__(name)
-        self._input_ports = [InputPort(0, self)]
-        self._output_ports = [OutputPort(0, self)]
+    def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = "", latency: Optional[int] = None, latency_offsets: Optional[Dict[str, int]] = None):
+        """Construct a Min operation."""
+        super().__init__(input_count=2, output_count=1, name=name, input_sources=[src0, src1],
+                         latency=latency, latency_offsets=latency_offsets)
 
-        if source1 is not None:
-            self._input_ports[0].connect(source1)
-
-    def evaluate(self, a):
-        return conjugate(a)
+    @classmethod
+    def type_name(cls) -> TypeName:
+        return "min"
 
-    @property
-    def type_name(self) -> TypeName:
-        return "conj"
+    def evaluate(self, a, b):
+        assert not isinstance(a, complex) and not isinstance(b, complex), \
+            ("core_operations.Min does not support complex numbers.")
+        return a if a < b else b
 
 
 class Max(AbstractOperation):
     """Binary max operation.
-    TODO: More info.
+
+    Gives the maximum value of two inputs.
+    NOTE: Non-real numbers are not supported.
+
+    output(0): max(input(0), input(1))
     """
 
-    def __init__(self, source1: OutputPort = None, source2: OutputPort = None, name: Name = ""):
-        super().__init__(name)
-        self._input_ports = [InputPort(0, self), InputPort(1, self)]
-        self._output_ports = [OutputPort(0, self)]
+    def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = "", latency: Optional[int] = None, latency_offsets: Optional[Dict[str, int]] = None):
+        """Construct a Max operation."""
+        super().__init__(input_count=2, output_count=1, name=name, input_sources=[src0, src1],
+                         latency=latency, latency_offsets=latency_offsets)
 
-        if source1 is not None:
-            self._input_ports[0].connect(source1)
-        if source2 is not None:
-            self._input_ports[1].connect(source2)
+    @classmethod
+    def type_name(cls) -> TypeName:
+        return "max"
 
     def evaluate(self, a, b):
         assert not isinstance(a, complex) and not isinstance(b, complex), \
             ("core_operations.Max does not support complex numbers.")
         return a if a > b else b
 
-    @property
-    def type_name(self) -> TypeName:
-        return "max"
 
+class SquareRoot(AbstractOperation):
+    """Square root operation.
 
-class Min(AbstractOperation):
-    """Binary min operation.
-    TODO: More info.
+    Gives the square root of its input.
+
+    output(0): sqrt(input(0))
     """
 
-    def __init__(self, source1: OutputPort = None, source2: OutputPort = None, name: Name = ""):
-        super().__init__(name)
-        self._input_ports = [InputPort(0, self), InputPort(1, self)]
-        self._output_ports = [OutputPort(0, self)]
+    def __init__(self, src0: Optional[SignalSourceProvider] = None, name: Name = "", latency: Optional[int] = None, latency_offsets: Optional[Dict[str, int]] = None):
+        """Construct a SquareRoot operation."""
+        super().__init__(input_count=1, output_count=1, name=name, input_sources=[src0],
+                         latency=latency, latency_offsets=latency_offsets)
 
-        if source1 is not None:
-            self._input_ports[0].connect(source1)
-        if source2 is not None:
-            self._input_ports[1].connect(source2)
+    @classmethod
+    def type_name(cls) -> TypeName:
+        return "sqrt"
 
-    def evaluate(self, a, b):
-        assert not isinstance(a, complex) and not isinstance(b, complex), \
-            ("core_operations.Min does not support complex numbers.")
-        return a if a < b else b
+    def evaluate(self, a):
+        return sqrt(complex(a))
 
-    @property
-    def type_name(self) -> TypeName:
-        return "min"
 
+class ComplexConjugate(AbstractOperation):
+    """Complex conjugate operation.
 
-class Absolute(AbstractOperation):
-    """Unary absolute value operation.
-    TODO: More info.
+    Gives the complex conjugate of its input.
+
+    output(0): conj(input(0))
     """
 
-    def __init__(self, source1: OutputPort = None, name: Name = ""):
-        super().__init__(name)
-        self._input_ports = [InputPort(0, self)]
-        self._output_ports = [OutputPort(0, self)]
+    def __init__(self, src0: Optional[SignalSourceProvider] = None, name: Name = "", latency: Optional[int] = None, latency_offsets: Optional[Dict[str, int]] = None):
+        """Construct a ComplexConjugate operation."""
+        super().__init__(input_count=1, output_count=1, name=name, input_sources=[src0],
+                         latency=latency, latency_offsets=latency_offsets)
 
-        if source1 is not None:
-            self._input_ports[0].connect(source1)
+    @classmethod
+    def type_name(cls) -> TypeName:
+        return "conj"
 
     def evaluate(self, a):
-        return np_abs(a)
+        return conjugate(a)
 
-    @property
-    def type_name(self) -> TypeName:
-        return "abs"
 
+class Absolute(AbstractOperation):
+    """Absolute value operation.
 
-class ConstantMultiplication(AbstractOperation):
-    """Unary constant multiplication operation.
-    TODO: More info.
+    Gives the absolute value of its input.
+
+    output(0): abs(input(0))
     """
 
-    def __init__(self, coefficient: Number, source1: OutputPort = None, name: Name = ""):
-        super().__init__(name)
-        self._input_ports = [InputPort(0, self)]
-        self._output_ports = [OutputPort(0, self)]
-        self._parameters["coefficient"] = coefficient
+    def __init__(self, src0: Optional[SignalSourceProvider] = None, name: Name = "", latency: Optional[int] = None, latency_offsets: Optional[Dict[str, int]] = None):
+        """Construct an Absolute operation."""
+        super().__init__(input_count=1, output_count=1, name=name, input_sources=[src0],
+                         latency=latency, latency_offsets=latency_offsets)
 
-        if source1 is not None:
-            self._input_ports[0].connect(source1)
+    @classmethod
+    def type_name(cls) -> TypeName:
+        return "abs"
 
     def evaluate(self, a):
-        return a * self.param("coefficient")
+        return np_abs(a)
 
-    @property
-    def type_name(self) -> TypeName:
-        return "cmul"
 
+class ConstantMultiplication(AbstractOperation):
+    """Constant multiplication operation.
+
+    Gives the result of multiplying its input by a specified value.
 
-class ConstantAddition(AbstractOperation):
-    """Unary constant addition operation.
-    TODO: More info.
+    output(0): self.param("value") * input(0)
     """
 
-    def __init__(self, coefficient: Number, source1: OutputPort = None, name: Name = ""):
-        super().__init__(name)
-        self._input_ports = [InputPort(0, self)]
-        self._output_ports = [OutputPort(0, self)]
-        self._parameters["coefficient"] = coefficient
+    def __init__(self, value: Number = 0, src0: Optional[SignalSourceProvider] = None, name: Name = "", latency: Optional[int] = None, latency_offsets: Optional[Dict[str, int]] = None):
+        """Construct a ConstantMultiplication operation with the given value."""
+        super().__init__(input_count=1, output_count=1, name=name, input_sources=[src0],
+                         latency=latency, latency_offsets=latency_offsets)
+        self.set_param("value", value)
 
-        if source1 is not None:
-            self._input_ports[0].connect(source1)
+    @classmethod
+    def type_name(cls) -> TypeName:
+        return "cmul"
 
     def evaluate(self, a):
-        return a + self.param("coefficient")
+        return a * self.param("value")
 
     @property
-    def type_name(self) -> TypeName:
-        return "cadd"
+    def value(self) -> Number:
+        """Get the constant value of this operation."""
+        return self.param("value")
+
+    @value.setter
+    def value(self, value: Number) -> None:
+        """Set the constant value of this operation."""
+        return self.set_param("value", value)
 
 
-class ConstantSubtraction(AbstractOperation):
-    """Unary constant subtraction operation.
-    TODO: More info.
+class Butterfly(AbstractOperation):
+    """Butterfly operation.
+
+    Gives the result of adding its two inputs, as well as the result of
+    subtracting the second input from the first one.
+
+    output(0): input(0) + input(1)
+    output(1): input(0) - input(1)
     """
 
-    def __init__(self, coefficient: Number, source1: OutputPort = None, name: Name = ""):
-        super().__init__(name)
-        self._input_ports = [InputPort(0, self)]
-        self._output_ports = [OutputPort(0, self)]
-        self._parameters["coefficient"] = coefficient
+    def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, name: Name = "", latency: Optional[int] = None, latency_offsets: Optional[Dict[str, int]] = None):
+        """Construct a Butterfly operation."""
+        super().__init__(input_count=2, output_count=2, name=name, input_sources=[src0, src1],
+                         latency=latency, latency_offsets=latency_offsets)
 
-        if source1 is not None:
-            self._input_ports[0].connect(source1)
+    @classmethod
+    def type_name(cls) -> TypeName:
+        return "bfly"
 
-    def evaluate(self, a):
-        return a - self.param("coefficient")
+    def evaluate(self, a, b):
+        return a + b, a - b
 
-    @property
-    def type_name(self) -> TypeName:
-        return "csub"
 
+class MAD(AbstractOperation):
+    """Multiply-add operation.
 
-class ConstantDivision(AbstractOperation):
-    """Unary constant division operation.
-    TODO: More info.
-    """
+    Gives the result of multiplying the first input by the second input and
+    then adding the third input.
 
-    def __init__(self, coefficient: Number, source1: OutputPort = None, name: Name = ""):
-        super().__init__(name)
-        self._input_ports = [InputPort(0, self)]
-        self._output_ports = [OutputPort(0, self)]
-        self._parameters["coefficient"] = coefficient
+    output(0): (input(0) * input(1)) + input(2)
+    """
 
-        if source1 is not None:
-            self._input_ports[0].connect(source1)
+    def __init__(self, src0: Optional[SignalSourceProvider] = None, src1: Optional[SignalSourceProvider] = None, src2: Optional[SignalSourceProvider] = None, name: Name = "", latency: Optional[int] = None, latency_offsets: Optional[Dict[str, int]] = None):
+        """Construct a MAD operation."""
+        super().__init__(input_count=3, output_count=1, name=name, input_sources=[src0, src1, src2],
+                         latency=latency, latency_offsets=latency_offsets)
 
-    def evaluate(self, a):
-        return a / self.param("coefficient")
+    @classmethod
+    def type_name(cls) -> TypeName:
+        return "mad"
 
-    @property
-    def type_name(self) -> TypeName:
-        return "cdiv"
+    def evaluate(self, a, b, c):
+        return a * b + c
diff --git a/b_asic/graph_component.py b/b_asic/graph_component.py
index 1987d4491e12089fec401eedc14fb91a7274252e..8a0b4a9a6e7385ed2ababd28cc5b4619c1c586d7 100644
--- a/b_asic/graph_component.py
+++ b/b_asic/graph_component.py
@@ -1,49 +1,125 @@
-"""@package docstring
-B-ASIC Operation Module.
-TODO: More info.
+"""B-ASIC Graph Component Module.
+
+Contains the base for all components with an ID in a signal flow graph.
 """
 
 from abc import ABC, abstractmethod
-from typing import NewType
+from collections import deque
+from copy import copy, deepcopy
+from typing import NewType, Any, Dict, Mapping, Iterable, Generator
+
 
 Name = NewType("Name", str)
 TypeName = NewType("TypeName", str)
+GraphID = NewType("GraphID", str)
+GraphIDNumber = NewType("GraphIDNumber", int)
 
 
 class GraphComponent(ABC):
     """Graph component interface.
-    TODO: More info.
+
+    Each graph component has a type name, a name and a unique ID.
+    Graph components also contain parameters and provide an interface for
+    copying that automatically copies each parameter in its default
+    implementation.
+
+    Graph components also provide an interface for traversing connected graph
+    components and accessing their direct neighbors.
     """
 
-    @property
+    @classmethod
     @abstractmethod
-    def type_name(self) -> TypeName:
-        """Return the type name of the graph component"""
+    def type_name(cls) -> TypeName:
+        """Get the type name of this graph component"""
         raise NotImplementedError
 
     @property
     @abstractmethod
     def name(self) -> Name:
-        """Return the name of the graph component."""
+        """Get the name of this graph component."""
         raise NotImplementedError
 
     @name.setter
     @abstractmethod
     def name(self, name: Name) -> None:
-        """Set the name of the graph component to the entered name."""
+        """Set the name of this graph component to the given name."""
+        raise NotImplementedError
+
+    @property
+    @abstractmethod
+    def graph_id(self) -> GraphID:
+        """Get the graph id of this graph component."""
+        raise NotImplementedError
+
+    @graph_id.setter
+    @abstractmethod
+    def graph_id(self, graph_id: GraphID) -> None:
+        """Set the graph id of this graph component to the given id.
+        Note that this id will be ignored if this component is used to create a new graph,
+        and that a new local id will be generated for it instead."""
+        raise NotImplementedError
+
+    @property
+    @abstractmethod
+    def params(self) -> Mapping[str, Any]:
+        """Get a dictionary of all parameter values."""
+        raise NotImplementedError
+
+    @abstractmethod
+    def param(self, name: str) -> Any:
+        """Get the value of a parameter.
+        Returns None if the parameter is not defined.
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def set_param(self, name: str, value: Any) -> None:
+        """Set the value of a parameter.
+        Adds the parameter if it is not already defined.
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def copy_component(self, *args, **kwargs) -> "GraphComponent":
+        """Get a new instance of this graph component type with the same name, id and parameters."""
+        raise NotImplementedError
+
+    @property
+    @abstractmethod
+    def neighbors(self) -> Iterable["GraphComponent"]:
+        """Get all components that are directly connected to this operation."""
+        raise NotImplementedError
+
+    @abstractmethod
+    def traverse(self) -> Generator["GraphComponent", None, None]:
+        """Get a generator that recursively iterates through all components that are connected to this operation,
+        as well as the ones that they are connected to.
+        """
         raise NotImplementedError
 
 
 class AbstractGraphComponent(GraphComponent):
-    """Abstract Graph Component class which is a component of a signal flow graph.
+    """Generic abstract graph component base class.
 
-    TODO: More info.
+    Concrete graph components should normally derive from this to get the
+    default behavior.
     """
 
     _name: Name
+    _graph_id: GraphID
+    _parameters: Dict[str, Any]
 
     def __init__(self, name: Name = ""):
+        """Construct a graph component."""
         self._name = name
+        self._graph_id = ""
+        self._parameters = {}
+
+    def __str__(self) -> str:
+        """Get a string representation of this graph component."""
+        return f"id: {self.graph_id if self.graph_id else 'no_id'}, \tname: {self.name if self.name else 'no_name'}" + \
+            "".join((f", \t{key}: {str(param)}" for key,
+                     param in self._parameters.items()))
 
     @property
     def name(self) -> Name:
@@ -52,3 +128,42 @@ class AbstractGraphComponent(GraphComponent):
     @name.setter
     def name(self, name: Name) -> None:
         self._name = name
+
+    @property
+    def graph_id(self) -> GraphID:
+        return self._graph_id
+
+    @graph_id.setter
+    def graph_id(self, graph_id: GraphID) -> None:
+        self._graph_id = graph_id
+
+    @property
+    def params(self) -> Mapping[str, Any]:
+        return self._parameters.copy()
+
+    def param(self, name: str) -> Any:
+        return self._parameters.get(name)
+
+    def set_param(self, name: str, value: Any) -> None:
+        self._parameters[name] = value
+
+    def copy_component(self, *args, **kwargs) -> GraphComponent:
+        new_component = self.__class__(*args, **kwargs)
+        new_component.name = copy(self.name)
+        new_component.graph_id = copy(self.graph_id)
+        for name, value in self.params.items():
+            new_component.set_param(copy(name), deepcopy(
+                value))  # pylint: disable=no-member
+        return new_component
+
+    def traverse(self) -> Generator[GraphComponent, None, None]:
+        # Breadth first search.
+        visited = {self}
+        fontier = deque([self])
+        while fontier:
+            component = fontier.popleft()
+            yield component
+            for neighbor in component.neighbors:
+                if neighbor not in visited:
+                    visited.add(neighbor)
+                    fontier.append(neighbor)
diff --git a/b_asic/graph_id.py b/b_asic/graph_id.py
deleted file mode 100644
index 8da6a9d4af6a1bee25125904527a2fd3a374ab90..0000000000000000000000000000000000000000
--- a/b_asic/graph_id.py
+++ /dev/null
@@ -1,26 +0,0 @@
-"""@package docstring
-B-ASIC Graph ID module for handling IDs of different objects in a graph.
-TODO: More info
-"""
-
-from collections import defaultdict
-from typing import NewType, DefaultDict
-
-GraphID = NewType("GraphID", str)
-GraphIDType = NewType("GraphIDType", str)
-GraphIDNumber = NewType("GraphIDNumber", int)
-
-
-class GraphIDGenerator:
-    """A class that generates Graph IDs for objects."""
-
-    _next_id_number: DefaultDict[GraphIDType, GraphIDNumber]
-
-    def __init__(self):
-        self._next_id_number = defaultdict(lambda: 1)       # Initalises every key element to 1
-
-    def get_next_id(self, graph_id_type: GraphIDType) -> GraphID:
-        """Return the next graph id for a certain graph id type."""
-        graph_id = graph_id_type + str(self._next_id_number[graph_id_type])
-        self._next_id_number[graph_id_type] += 1            # Increase the current id number
-        return graph_id
diff --git a/b_asic/operation.py b/b_asic/operation.py
index 5578e3c48edcf15594d6d1cd71e71a17521eca25..2ce783963d883f65c1ec29d3dddb7bf0eeccbd6f 100644
--- a/b_asic/operation.py
+++ b/b_asic/operation.py
@@ -1,268 +1,616 @@
-"""@package docstring
-B-ASIC Operation Module.
-TODO: More info.
+"""B-ASIC Operation Module.
+
+Contains the base for operations that are used by B-ASIC.
 """
 
+from b_asic.signal import Signal
+from b_asic.port import SignalSourceProvider, InputPort, OutputPort
+from b_asic.graph_component import GraphComponent, AbstractGraphComponent, Name
+import itertools as it
+from math import trunc
+import collections
+
 from abc import abstractmethod
 from numbers import Number
-from typing import List, Dict, Optional, Any, Set, TYPE_CHECKING
-from collections import deque
+from typing import NewType, List, Dict, Sequence, Iterable, Mapping, MutableMapping, Optional, Any, Set, Union
 
-from b_asic.graph_component import GraphComponent, AbstractGraphComponent, Name
-from b_asic.simulation import SimulationState, OperationState
-from b_asic.signal import Signal
 
-if TYPE_CHECKING:
-    from b_asic.port import InputPort, OutputPort
+ResultKey = NewType("ResultKey", str)
+ResultMap = Mapping[ResultKey, Optional[Number]]
+MutableResultMap = MutableMapping[ResultKey, Optional[Number]]
+DelayMap = Mapping[ResultKey, Number]
+MutableDelayMap = MutableMapping[ResultKey, Number]
 
 
-class Operation(GraphComponent):
+class Operation(GraphComponent, SignalSourceProvider):
     """Operation interface.
-    TODO: More info.
+
+    Operations are graph components that perform a certain function.
+    They are connected to eachother by signals through their input/output
+    ports.
+
+    Operations can be evaluated independently using evaluate_output().
+    Operations may specify how to truncate inputs through truncate_input().
     """
 
     @abstractmethod
-    def inputs(self) -> "List[InputPort]":
-        """Get a list of all input ports."""
+    def __add__(self, src: Union[SignalSourceProvider, Number]) -> "Addition":
+        """Overloads the addition operator to make it return a new Addition operation
+        object that is connected to the self and other objects.
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def __radd__(self, src: Union[SignalSourceProvider, Number]) -> "Addition":
+        """Overloads the addition operator to make it return a new Addition operation
+        object that is connected to the self and other objects.
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def __sub__(self, src: Union[SignalSourceProvider, Number]) -> "Subtraction":
+        """Overloads the subtraction operator to make it return a new Subtraction operation
+        object that is connected to the self and other objects.
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def __rsub__(self, src: Union[SignalSourceProvider, Number]) -> "Subtraction":
+        """Overloads the subtraction operator to make it return a new Subtraction operation
+        object that is connected to the self and other objects.
+        """
         raise NotImplementedError
 
     @abstractmethod
-    def outputs(self) -> "List[OutputPort]":
-        """Get a list of all output ports."""
+    def __mul__(self, src: Union[SignalSourceProvider, Number]) -> "Union[Multiplication, ConstantMultiplication]":
+        """Overloads the multiplication operator to make it return a new Multiplication operation
+        object that is connected to the self and other objects. If other is a number then
+        returns a ConstantMultiplication operation object instead.
+        """
         raise NotImplementedError
 
+    @abstractmethod
+    def __rmul__(self, src: Union[SignalSourceProvider, Number]) -> "Union[Multiplication, ConstantMultiplication]":
+        """Overloads the multiplication operator to make it return a new Multiplication operation
+        object that is connected to the self and other objects. If other is a number then
+        returns a ConstantMultiplication operation object instead.
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def __truediv__(self, src: Union[SignalSourceProvider, Number]) -> "Division":
+        """Overloads the division operator to make it return a new Division operation
+        object that is connected to the self and other objects.
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def __rtruediv__(self, src: Union[SignalSourceProvider, Number]) -> "Division":
+        """Overloads the division operator to make it return a new Division operation
+        object that is connected to the self and other objects.
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def __lshift__(self, src: SignalSourceProvider) -> Signal:
+        """Overloads the left shift operator to make it connect the provided signal source
+        to this operation's input, assuming it has exactly 1 input port.
+        Returns the new signal.
+        """
+        raise NotImplementedError
+
+    @property
     @abstractmethod
     def input_count(self) -> int:
         """Get the number of input ports."""
         raise NotImplementedError
 
+    @property
     @abstractmethod
     def output_count(self) -> int:
         """Get the number of output ports."""
         raise NotImplementedError
 
     @abstractmethod
-    def input(self, i: int) -> "InputPort":
-        """Get the input port at index i."""
+    def input(self, index: int) -> InputPort:
+        """Get the input port at the given index."""
         raise NotImplementedError
 
     @abstractmethod
-    def output(self, i: int) -> "OutputPort":
-        """Get the output port at index i."""
+    def output(self, index: int) -> OutputPort:
+        """Get the output port at the given index."""
         raise NotImplementedError
 
+    @property
     @abstractmethod
-    def params(self) -> Dict[str, Optional[Any]]:
-        """Get a dictionary of all parameter values."""
+    def inputs(self) -> Sequence[InputPort]:
+        """Get all input ports."""
         raise NotImplementedError
 
+    @property
+    @abstractmethod
+    def outputs(self) -> Sequence[OutputPort]:
+        """Get all output ports."""
+        raise NotImplementedError
+
+    @property
+    @abstractmethod
+    def input_signals(self) -> Iterable[Signal]:
+        """Get all the signals that are connected to this operation's input ports,
+        in no particular order.
+        """
+        raise NotImplementedError
+
+    @property
     @abstractmethod
-    def param(self, name: str) -> Optional[Any]:
-        """Get the value of a parameter.
-        Returns None if the parameter is not defined.
+    def output_signals(self) -> Iterable[Signal]:
+        """Get all the signals that are connected to this operation's output ports,
+        in no particular order.
         """
         raise NotImplementedError
 
     @abstractmethod
-    def set_param(self, name: str, value: Any) -> None:
-        """Set the value of a parameter.
-        The parameter must be defined.
+    def key(self, index: int, prefix: str = "") -> ResultKey:
+        """Get the key used to access the output of a certain output of this operation
+        from the output parameter passed to current_output(s) or evaluate_output(s).
         """
         raise NotImplementedError
 
     @abstractmethod
-    def evaluate_outputs(self, state: "SimulationState") -> List[Number]:
-        """Simulate the circuit until its iteration count matches that of the simulation state,
-        then return the resulting output vector.
+    def current_output(self, index: int, delays: Optional[DelayMap] = None, prefix: str = "") -> Optional[Number]:
+        """Get the current output at the given index of this operation, if available.
+        The delays parameter will be used for lookup.
+        The prefix parameter will be used as a prefix for the key string when looking for delays.
+        See also: current_outputs, evaluate_output, evaluate_outputs.
         """
         raise NotImplementedError
 
     @abstractmethod
-    def split(self) -> "List[Operation]":
+    def evaluate_output(self, index: int, input_values: Sequence[Number], results: Optional[MutableResultMap] = None, delays: Optional[MutableDelayMap] = None, prefix: str = "", bits_override: Optional[int] = None, truncate: bool = True) -> Number:
+        """Evaluate the output at the given index of this operation with the given input values.
+        The results parameter will be used to store any results (including intermediate results) for caching.
+        The delays parameter will be used to get the current value of any intermediate delays that are encountered, and be updated with their new values.
+        The prefix parameter will be used as a prefix for the key string when storing results/delays.
+        The bits_override parameter specifies a word length override when truncating inputs which ignores the word length specified by the input signal.
+        The truncate parameter specifies whether input truncation should be enabled in the first place. If set to False, input values will be used driectly without any bit truncation.
+        See also: evaluate_outputs, current_output, current_outputs.
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def current_outputs(self, delays: Optional[DelayMap] = None, prefix: str = "") -> Sequence[Optional[Number]]:
+        """Get all current outputs of this operation, if available.
+        See current_output for more information.
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def evaluate_outputs(self, input_values: Sequence[Number], results: Optional[MutableResultMap] = None, delays: Optional[MutableDelayMap] = None, prefix: str = "", bits_override: Optional[int] = None, truncate: bool = True) -> Sequence[Number]:
+        """Evaluate all outputs of this operation given the input values.
+        See evaluate_output for more information.
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def split(self) -> Iterable["Operation"]:
         """Split the operation into multiple operations.
         If splitting is not possible, this may return a list containing only the operation itself.
         """
         raise NotImplementedError
 
+    @abstractmethod
+    def to_sfg(self) -> "SFG":
+        """Convert the operation into its corresponding SFG.
+        If the operation is composed by multiple operations, the operation will be split.
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def inputs_required_for_output(self, output_index: int) -> Iterable[int]:
+        """Get the input indices of all inputs in this operation whose values are required in order to evaluate the output at the given output index."""
+        raise NotImplementedError
+
+    @abstractmethod
+    def truncate_input(self, index: int, value: Number, bits: int) -> Number:
+        """Truncate the value to be used as input at the given index to a certain bit length."""
+        raise NotImplementedError
+
     @property
     @abstractmethod
-    def neighbors(self) -> "List[Operation]":
-        """Return all operations that are connected by signals to this operation.
-        If no neighbors are found, this returns an empty list.
+    def latency(self) -> int:
+        """Get the latency of the operation, which is the longest time it takes from one of
+        the operations inputport to one of the operations outputport.
+        """
+        raise NotImplementedError
+
+    @property
+    @abstractmethod
+    def latency_offsets(self) -> Sequence[Sequence[int]]:
+        """Get a nested list with all the operations ports latency-offsets, the first list contains the
+        latency-offsets of the operations input ports, the second list contains the latency-offsets of
+        the operations output ports.
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def set_latency(self, latency: int) -> None:
+        """Sets the latency of the operation to the specified integer value  by setting the
+        latency-offsets of operations input ports to 0 and the latency-offsets of the operations
+        output ports to the specified value. The latency cannot be a negative integers.
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def set_latency_offsets(self, latency_offsets: Dict[str, int]) -> None:
+        """Sets the latency-offsets for the operations ports specified in the latency_offsets dictionary.
+        The latency offsets dictionary should be {'in0': 2, 'out1': 4} if you want to set the latency offset
+        for the inport port with index 0 to 2, and the latency offset of the output port with index 1 to 4.
         """
         raise NotImplementedError
 
 
 class AbstractOperation(Operation, AbstractGraphComponent):
-    """Generic abstract operation class which most implementations will derive from.
-    TODO: More info.
+    """Generic abstract operation base class.
+
+    Concrete operations should normally derive from this to get the default
+    behavior.
     """
 
-    _input_ports: List["InputPort"]
-    _output_ports: List["OutputPort"]
-    _parameters: Dict[str, Optional[Any]]
+    _input_ports: List[InputPort]
+    _output_ports: List[OutputPort]
 
-    def __init__(self, name: Name = ""):
+    def __init__(self, input_count: int, output_count: int, name: Name = "", input_sources: Optional[Sequence[Optional[SignalSourceProvider]]] = None, latency: Optional[int] = None, latency_offsets: Optional[Dict[str, int]] = None):
+        """Construct an operation with the given input/output count.
+
+        A list of input sources may be specified to automatically connect
+        to the input ports.
+        If provided, the number of sources must match the number of inputs.
+
+        The latency offsets may also be specified to be initialized.
+        """
         super().__init__(name)
-        self._input_ports = []
-        self._output_ports = []
-        self._parameters = {}
+
+        self._input_ports = [InputPort(self, i) for i in range(input_count)]
+        self._output_ports = [OutputPort(self, i) for i in range(output_count)]
+
+        # Connect given input sources, if any.
+        if input_sources is not None:
+            source_count = len(input_sources)
+            if source_count != input_count:
+                raise ValueError(
+                    f"Wrong number of input sources supplied to Operation (expected {input_count}, got {source_count})")
+            for i, src in enumerate(input_sources):
+                if src is not None:
+                    self._input_ports[i].connect(src.source)
+
+        ports_without_latency_offset = set(([f"in{i}" for i in range(self.input_count)] +
+                                            [f"out{i}" for i in range(self.output_count)]))
+
+        if latency_offsets is not None:
+            self.set_latency_offsets(latency_offsets)
+
+        if latency is not None:
+            # Set the latency of the rest of ports with no latency_offset.
+            assert latency >= 0, "Negative latency entered"
+            for inp in self.inputs:
+                if inp.latency_offset is None:
+                    inp.latency_offset = 0
+            for outp in self.outputs:
+                if outp.latency_offset is None:
+                    outp.latency_offset = latency
 
     @abstractmethod
     def evaluate(self, *inputs) -> Any:  # pylint: disable=arguments-differ
-        """Evaluate the operation and generate a list of output values given a
-        list of input values.
-        """
+        """Evaluate the operation and generate a list of output values given a list of input values."""
         raise NotImplementedError
 
-    def inputs(self) -> List["InputPort"]:
-        return self._input_ports.copy()
+    def __add__(self, src: Union[SignalSourceProvider, Number]) -> "Addition":
+        # Import here to avoid circular imports.
+        from b_asic.core_operations import Constant, Addition
+        return Addition(self, Constant(src) if isinstance(src, Number) else src)
+
+    def __radd__(self, src: Union[SignalSourceProvider, Number]) -> "Addition":
+        # Import here to avoid circular imports.
+        from b_asic.core_operations import Constant, Addition
+        return Addition(Constant(src) if isinstance(src, Number) else src, self)
+
+    def __sub__(self, src: Union[SignalSourceProvider, Number]) -> "Subtraction":
+        # Import here to avoid circular imports.
+        from b_asic.core_operations import Constant, Subtraction
+        return Subtraction(self, Constant(src) if isinstance(src, Number) else src)
+
+    def __rsub__(self, src: Union[SignalSourceProvider, Number]) -> "Subtraction":
+        # Import here to avoid circular imports.
+        from b_asic.core_operations import Constant, Subtraction
+        return Subtraction(Constant(src) if isinstance(src, Number) else src, self)
+
+    def __mul__(self, src: Union[SignalSourceProvider, Number]) -> "Union[Multiplication, ConstantMultiplication]":
+        # Import here to avoid circular imports.
+        from b_asic.core_operations import Multiplication, ConstantMultiplication
+        return ConstantMultiplication(src, self) if isinstance(src, Number) else Multiplication(self, src)
 
-    def outputs(self) -> List["OutputPort"]:
-        return self._output_ports.copy()
+    def __rmul__(self, src: Union[SignalSourceProvider, Number]) -> "Union[Multiplication, ConstantMultiplication]":
+        # Import here to avoid circular imports.
+        from b_asic.core_operations import Multiplication, ConstantMultiplication
+        return ConstantMultiplication(src, self) if isinstance(src, Number) else Multiplication(src, self)
+
+    def __truediv__(self, src: Union[SignalSourceProvider, Number]) -> "Division":
+        # Import here to avoid circular imports.
+        from b_asic.core_operations import Constant, Division
+        return Division(self, Constant(src) if isinstance(src, Number) else src)
+
+    def __rtruediv__(self, src: Union[SignalSourceProvider, Number]) -> "Division":
+        # Import here to avoid circular imports.
+        from b_asic.core_operations import Constant, Division
+        return Division(Constant(src) if isinstance(src, Number) else src, self)
+
+    def __lshift__(self, src: SignalSourceProvider) -> Signal:
+        if self.input_count != 1:
+            diff = "more" if self.input_count > 1 else "less"
+            raise TypeError(
+                f"{self.__class__.__name__} cannot be used as a destination because it has {diff} than 1 input")
+        return self.input(0).connect(src)
+
+    def __str__(self) -> str:
+        """Get a string representation of this operation."""
+        inputs_dict = dict()
+        for i, port in enumerate(self.inputs):
+            if port.signal_count == 0:
+                inputs_dict[i] = '-'
+                break
+            dict_ele = []
+            for signal in port.signals:
+                if signal.source:
+                    if signal.source.operation.graph_id:
+                        dict_ele.append(signal.source.operation.graph_id)
+                    else:
+                        dict_ele.append("no_id")
+                else:
+                    if signal.graph_id:
+                        dict_ele.append(signal.graph_id)
+                    else:
+                        dict_ele.append("no_id")
+            inputs_dict[i] = dict_ele
+
+        outputs_dict = dict()
+        for i, port in enumerate(self.outputs):
+            if port.signal_count == 0:
+                outputs_dict[i] = '-'
+                break
+            dict_ele = []
+            for signal in port.signals:
+                if signal.destination:
+                    if signal.destination.operation.graph_id:
+                        dict_ele.append(signal.destination.operation.graph_id)
+                    else:
+                        dict_ele.append("no_id")
+                else:
+                    if signal.graph_id:
+                        dict_ele.append(signal.graph_id)
+                    else:
+                        dict_ele.append("no_id")
+            outputs_dict[i] = dict_ele
+
+        return super().__str__() + f", \tinputs: {str(inputs_dict)}, \toutputs: {str(outputs_dict)}"
 
+    @property
     def input_count(self) -> int:
         return len(self._input_ports)
 
+    @property
     def output_count(self) -> int:
         return len(self._output_ports)
 
-    def input(self, i: int) -> "InputPort":
-        return self._input_ports[i]
-
-    def output(self, i: int) -> "OutputPort":
-        return self._output_ports[i]
-
-    def params(self) -> Dict[str, Optional[Any]]:
-        return self._parameters.copy()
-
-    def param(self, name: str) -> Optional[Any]:
-        return self._parameters.get(name)
-
-    def set_param(self, name: str, value: Any) -> None:
-        assert name in self._parameters  # TODO: Error message.
-        self._parameters[name] = value
-
-    def evaluate_outputs(self, state: SimulationState) -> List[Number]:
-        # TODO: Check implementation.
-        input_count: int = self.input_count()
-        output_count: int = self.output_count()
-        assert input_count == len(self._input_ports)  # TODO: Error message.
-        assert output_count == len(self._output_ports)  # TODO: Error message.
-
-        self_state: OperationState = state.operation_states[self]
-
-        while self_state.iteration < state.iteration:
-            input_values: List[Number] = [0] * input_count
-            for i in range(input_count):
-                source: Signal = self._input_ports[i].signal
-                input_values[i] = source.operation.evaluate_outputs(state)[
-                    source.port_index]
-
-            self_state.output_values = self.evaluate(input_values)
-            # TODO: Error message.
-            assert len(self_state.output_values) == output_count
-            self_state.iteration += 1
-            for i in range(output_count):
-                for signal in self._output_ports[i].signals():
-                    destination: Signal = signal.destination
-                    destination.evaluate_outputs(state)
-
-        return self_state.output_values
-
-    def split(self) -> List[Operation]:
-        # TODO: Check implementation.
-        results = self.evaluate(self._input_ports)
-        if all(isinstance(e, Operation) for e in results):
-            return results
-        return [self]
+    def input(self, index: int) -> InputPort:
+        return self._input_ports[index]
+
+    def output(self, index: int) -> OutputPort:
+        return self._output_ports[index]
 
     @property
-    def neighbors(self) -> List[Operation]:
-        neighbors: List[Operation] = []
-        for port in self._input_ports:
-            for signal in port.signals:
-                neighbors.append(signal.source.operation)
+    def inputs(self) -> Sequence[InputPort]:
+        return self._input_ports
 
-        for port in self._output_ports:
-            for signal in port.signals:
-                neighbors.append(signal.destination.operation)
-
-        return neighbors
-
-    def traverse(self) -> Operation:
-        """Traverse the operation tree and return a generator with start point in the operation."""
-        return self._breadth_first_search()
-
-    def _breadth_first_search(self) -> Operation:
-        """Use breadth first search to traverse the operation tree."""
-        visited: Set[Operation] = {self}
-        queue = deque([self])
-        while queue:
-            operation = queue.popleft()
-            yield operation
-            for n_operation in operation.neighbors:
-                if n_operation not in visited:
-                    visited.add(n_operation)
-                    queue.append(n_operation)
-
-    def __add__(self, other):
-        """Overloads the addition operator to make it return a new Addition operation
-        object that is connected to the self and other objects. If other is a number then
-        returns a ConstantAddition operation object instead.
-        """
-        # Import here to avoid circular imports.
-        from b_asic.core_operations import Addition, ConstantAddition
+    @property
+    def outputs(self) -> Sequence[OutputPort]:
+        return self._output_ports
+
+    @property
+    def input_signals(self) -> Iterable[Signal]:
+        result = []
+        for p in self.inputs:
+            for s in p.signals:
+                result.append(s)
+        return result
 
-        if isinstance(other, Operation):
-            return Addition(self.output(0), other.output(0))
-        elif isinstance(other, Number):
-            return ConstantAddition(other, self.output(0))
+    @property
+    def output_signals(self) -> Iterable[Signal]:
+        result = []
+        for p in self.outputs:
+            for s in p.signals:
+                result.append(s)
+        return result
+
+    def key(self, index: int, prefix: str = "") -> ResultKey:
+        key = prefix
+        if self.output_count != 1:
+            if key:
+                key += "."
+            key += str(index)
+        elif not key:
+            key = str(index)
+        return key
+
+    def current_output(self, index: int, delays: Optional[DelayMap] = None, prefix: str = "") -> Optional[Number]:
+        return None
+
+    def evaluate_output(self, index: int, input_values: Sequence[Number], results: Optional[MutableResultMap] = None, delays: Optional[MutableDelayMap] = None, prefix: str = "", bits_override: Optional[int] = None, truncate: bool = True) -> Number:
+        if index < 0 or index >= self.output_count:
+            raise IndexError(
+                f"Output index out of range (expected 0-{self.output_count - 1}, got {index})")
+        if len(input_values) != self.input_count:
+            raise ValueError(
+                f"Wrong number of input values supplied to operation (expected {self.input_count}, got {len(input_values)})")
+
+        values = self.evaluate(
+            *(self.truncate_inputs(input_values, bits_override) if truncate else input_values))
+        if isinstance(values, collections.abc.Sequence):
+            if len(values) != self.output_count:
+                raise RuntimeError(
+                    f"Operation evaluated to incorrect number of outputs (expected {self.output_count}, got {len(values)})")
+        elif isinstance(values, Number):
+            if self.output_count != 1:
+                raise RuntimeError(
+                    f"Operation evaluated to incorrect number of outputs (expected {self.output_count}, got 1)")
+            values = (values,)
         else:
-            raise TypeError("Other type is not an Operation or a Number.")
+            raise RuntimeError(
+                f"Operation evaluated to invalid type (expected Sequence/Number, got {values.__class__.__name__})")
 
-    def __sub__(self, other):
-        """Overloads the subtraction operator to make it return a new Subtraction operation
-        object that is connected to the self and other objects. If other is a number then
-        returns a ConstantSubtraction operation object instead.
-        """
-        # Import here to avoid circular imports.
-        from b_asic.core_operations import Subtraction, ConstantSubtraction
+        if results is not None:
+            for i in range(self.output_count):
+                results[self.key(i, prefix)] = values[i]
+        return values[index]
 
-        if isinstance(other, Operation):
-            return Subtraction(self.output(0), other.output(0))
-        elif isinstance(other, Number):
-            return ConstantSubtraction(other, self.output(0))
-        else:
-            raise TypeError("Other type is not an Operation or a Number.")
+    def current_outputs(self, delays: Optional[DelayMap] = None, prefix: str = "") -> Sequence[Optional[Number]]:
+        return [self.current_output(i, delays, prefix) for i in range(self.output_count)]
 
-    def __mul__(self, other):
-        """Overloads the multiplication operator to make it return a new Multiplication operation
-        object that is connected to the self and other objects. If other is a number then
-        returns a ConstantMultiplication operation object instead.
-        """
-        # Import here to avoid circular imports.
-        from b_asic.core_operations import Multiplication, ConstantMultiplication
+    def evaluate_outputs(self, input_values: Sequence[Number], results: Optional[MutableResultMap] = None, delays: Optional[MutableDelayMap] = None, prefix: str = "", bits_override: Optional[int] = None, truncate: bool = True) -> Sequence[Number]:
+        return [self.evaluate_output(i, input_values, results, delays, prefix, bits_override, truncate) for i in range(self.output_count)]
 
-        if isinstance(other, Operation):
-            return Multiplication(self.output(0), other.output(0))
-        elif isinstance(other, Number):
-            return ConstantMultiplication(other, self.output(0))
-        else:
-            raise TypeError("Other type is not an Operation or a Number.")
+    def split(self) -> Iterable[Operation]:
+        # Import here to avoid circular imports.
+        from b_asic.special_operations import Input
+        try:
+            result = self.evaluate(*([Input()] * self.input_count))
+            if isinstance(result, collections.Sequence) and all(isinstance(e, Operation) for e in result):
+                return result
+            if isinstance(result, Operation):
+                return [result]
+        except TypeError:
+            pass
+        except ValueError:
+            pass
+        return [self]
 
-    def __truediv__(self, other):
-        """Overloads the division operator to make it return a new Division operation
-        object that is connected to the self and other objects. If other is a number then
-        returns a ConstantDivision operation object instead.
-        """
+    def to_sfg(self) -> "SFG":
         # Import here to avoid circular imports.
-        from b_asic.core_operations import Division, ConstantDivision
+        from b_asic.special_operations import Input, Output
+        from b_asic.signal_flow_graph import SFG
+
+        inputs = [Input() for i in range(self.input_count)]
+
+        try:
+            last_operations = self.evaluate(*inputs)
+            if isinstance(last_operations, Operation):
+                last_operations = [last_operations]
+            outputs = [Output(o) for o in last_operations]
+        except TypeError:
+            operation_copy: Operation = self.copy_component()
+            inputs = []
+            for i in range(self.input_count):
+                _input = Input()
+                operation_copy.input(i).connect(_input)
+                inputs.append(_input)
+
+            outputs = [Output(operation_copy)]
+
+        return SFG(inputs=inputs, outputs=outputs)
+
+    def copy_component(self, *args, **kwargs) -> GraphComponent:
+        new_component: Operation = super().copy_component(*args, **kwargs)
+        for i, inp in enumerate(self.inputs):
+            new_component.input(i).latency_offset = inp.latency_offset
+        for i, outp in enumerate(self.outputs):
+            new_component.output(i).latency_offset = outp.latency_offset
+        return new_component
+
+    def inputs_required_for_output(self, output_index: int) -> Iterable[int]:
+        if output_index < 0 or output_index >= self.output_count:
+            raise IndexError(
+                f"Output index out of range (expected 0-{self.output_count - 1}, got {output_index})")
+        # By default, assume each output depends on all inputs.
+        return [i for i in range(self.input_count)]
 
-        if isinstance(other, Operation):
-            return Division(self.output(0), other.output(0))
-        elif isinstance(other, Number):
-            return ConstantDivision(other, self.output(0))
-        else:
-            raise TypeError("Other type is not an Operation or a Number.")
+    @property
+    def neighbors(self) -> Iterable[GraphComponent]:
+        return list(self.input_signals) + list(self.output_signals)
 
+    @property
+    def preceding_operations(self) -> Iterable[Operation]:
+        """Returns an Iterable of all Operations that are connected to this Operations input ports."""
+        return [signal.source.operation for signal in self.input_signals if signal.source]
+
+    @property
+    def subsequent_operations(self) -> Iterable[Operation]:
+        """Returns an Iterable of all Operations that are connected to this Operations output ports."""
+        return [signal.destination.operation for signal in self.output_signals if signal.destination]
+
+    @property
+    def source(self) -> OutputPort:
+        if self.output_count != 1:
+            diff = "more" if self.output_count > 1 else "less"
+            raise TypeError(
+                f"{self.__class__.__name__} cannot be used as an input source because it has {diff} than 1 output")
+        return self.output(0)
+
+    def truncate_input(self, index: int, value: Number, bits: int) -> Number:
+        return int(value) & ((2 ** bits) - 1)
+
+    def truncate_inputs(self, input_values: Sequence[Number], bits_override: Optional[int] = None) -> Sequence[Number]:
+        """Truncate the values to be used as inputs to the bit lengths specified by the respective signals connected to each input."""
+        args = []
+        for i, input_port in enumerate(self.inputs):
+            value = input_values[i]
+            bits = bits_override
+            if bits_override is None and input_port.signal_count >= 1:
+                bits = input_port.signals[0].bits
+            if bits_override is not None:
+                if isinstance(value, complex):
+                    raise TypeError(
+                        "Complex value cannot be truncated to {bits} bits as requested by the signal connected to input #{i}")
+                value = self.truncate_input(i, value, bits)
+            args.append(value)
+        return args
+
+    @property
+    def latency(self) -> int:
+        if None in [inp.latency_offset for inp in self.inputs] or None in [outp.latency_offset for outp in self.outputs]:
+            raise ValueError(
+                "All native offsets have to set to a non-negative value to calculate the latency.")
+
+        return max(((outp.latency_offset - inp.latency_offset) for outp, inp in it.product(self.outputs, self.inputs)))
+
+    @property
+    def latency_offsets(self) -> Sequence[Sequence[int]]:
+        latency_offsets = dict()
+
+        for i, inp in enumerate(self.inputs):
+            latency_offsets["in" + str(i)] = inp.latency_offset
+
+        for i, outp in enumerate(self.outputs):
+            latency_offsets["out" + str(i)] = outp.latency_offset
+
+        return latency_offsets
+
+    def set_latency(self, latency: int) -> None:
+        assert latency >= 0, "Negative latency entered."
+        for inport in self.inputs:
+            inport.latency_offset = 0
+        for outport in self.outputs:
+            outport.latency_offset = latency
+
+    def set_latency_offsets(self, latency_offsets: Dict[str, int]) -> None:
+        for port_str, latency_offset in latency_offsets.items():
+            port_str = port_str.lower()
+            if port_str.startswith("in"):
+                index_str = port_str[2:]
+                assert index_str.isdigit(), "Incorrectly formatted index in string, expected 'in' + index"
+                self.input(int(index_str)).latency_offset = latency_offset
+            elif port_str.startswith("out"):
+                index_str = port_str[3:]
+                assert index_str.isdigit(), "Incorrectly formatted index in string, expected 'out' + index"
+                self.output(int(index_str)).latency_offset = latency_offset
+            else:
+                raise ValueError(
+                    "Incorrectly formatted string, expected 'in' + index or 'out' + index")
diff --git a/b_asic/port.py b/b_asic/port.py
index c22053df1928cf2f6ea32c4880816d551b85836a..25197891d452b7844545cdae144979d73f6345d5 100644
--- a/b_asic/port.py
+++ b/b_asic/port.py
@@ -1,84 +1,79 @@
-"""@package docstring
-B-ASIC Port Module.
-TODO: More info.
+"""B-ASIC Port Module.
+
+Contains classes for managing the ports of operations.
 """
 
 from abc import ABC, abstractmethod
-from typing import NewType, Optional, List
+from copy import copy
+from typing import Optional, List, Iterable, TYPE_CHECKING
 
-from b_asic.operation import Operation
 from b_asic.signal import Signal
+from b_asic.graph_component import Name
+
+if TYPE_CHECKING:
+    from b_asic.operation import Operation
 
-PortIndex = NewType("PortIndex", int)
 
 class Port(ABC):
-    """Port Interface.
+    """Port interface.
 
-    TODO: More documentaiton?
+    Ports serve as connection points for connecting signals between operations.
+    They also store information about the latency of the corresponding
+    calculations of the operation.
+
+    Aside from connected signals, each port also provides a reference to the
+    parent operation that "owns" it as well as the operation's port index at
+    which the port resides.
     """
 
     @property
     @abstractmethod
-    def operation(self) -> Operation:
+    def operation(self) -> "Operation":
         """Return the connected operation."""
         raise NotImplementedError
 
     @property
     @abstractmethod
-    def index(self) -> PortIndex:
-        """Return the unique PortIndex."""
+    def index(self) -> int:
+        """Return the index of the port."""
         raise NotImplementedError
 
     @property
     @abstractmethod
-    def signals(self) -> List[Signal]:
-        """Return a list of all connected signals."""
+    def latency_offset(self) -> int:
+        """Get the latency_offset of the port."""
         raise NotImplementedError
 
+    @latency_offset.setter
     @abstractmethod
-    def signal(self, i: int = 0) -> Signal:
-        """Return the connected signal at index i.
-
-        Keyword argumens:
-        i: integer index of the signal requsted.
-        """
+    def latency_offset(self, latency_offset: int) -> None:
+        """Set the latency_offset of the port to the integer specified value."""
         raise NotImplementedError
 
     @property
-    @abstractmethod
-    def connected_ports(self) -> List["Port"]:
-        """Return a list of all connected Ports."""
-        raise NotImplementedError
-
     @abstractmethod
     def signal_count(self) -> int:
         """Return the number of connected signals."""
         raise NotImplementedError
 
+    @property
     @abstractmethod
-    def connect(self, port: "Port") -> Signal:
-        """Create and return a signal that is connected to this port and the entered
-        port and connect this port to the signal and the entered port to the signal."""
+    def signals(self) -> Iterable[Signal]:
+        """Return all connected signals."""
         raise NotImplementedError
 
     @abstractmethod
     def add_signal(self, signal: Signal) -> None:
         """Connect this port to the entered signal. If the entered signal isn't connected to
-        this port then connect the entered signal to the port aswell."""
-        raise NotImplementedError
-
-    @abstractmethod
-    def disconnect(self, port: "Port") -> None:
-        """Disconnect the entered port from the port by removing it from the ports signal.
-        If the entered port is still connected to this ports signal then disconnect the entered
-        port from the signal aswell."""
+        this port then connect the entered signal to the port as well.
+        """
         raise NotImplementedError
 
     @abstractmethod
     def remove_signal(self, signal: Signal) -> None:
         """Remove the signal that was entered from the Ports signals.
         If the entered signal still is connected to this port then disconnect the
-        entered signal from the port aswell.
+        entered signal from the port as well.
 
         Keyword arguments:
         - signal: Signal to remove.
@@ -92,132 +87,146 @@ class Port(ABC):
 
 
 class AbstractPort(Port):
-    """Abstract port class.
+    """Generic abstract port base class.
 
-    Handles functionality for port id and saves the connection to the parent operation.
+    Concrete ports should normally derive from this to get the default
+    behavior.
     """
 
+    _operation: "Operation"
     _index: int
-    _operation: Operation
+    _latency_offset: Optional[int]
 
-    def __init__(self, index: int, operation: Operation):
-        self._index = index
+    def __init__(self, operation: "Operation", index: int, latency_offset: Optional[int] = None):
+        """Construct a port of the given operation at the given port index."""
         self._operation = operation
+        self._index = index
+        self._latency_offset = latency_offset
 
     @property
-    def operation(self) -> Operation:
+    def operation(self) -> "Operation":
         return self._operation
 
     @property
-    def index(self) -> PortIndex:
+    def index(self) -> int:
         return self._index
 
+    @property
+    def latency_offset(self) -> int:
+        return self._latency_offset
+
+    @latency_offset.setter
+    def latency_offset(self, latency_offset: int):
+        self._latency_offset = latency_offset
+
+
+class SignalSourceProvider(ABC):
+    """Signal source provider interface.
+
+    Signal source providers give access to a single output port that can be
+    used to connect signals from.
+    """
+
+    @property
+    @abstractmethod
+    def source(self) -> "OutputPort":
+        """Get the main source port provided by this object."""
+        raise NotImplementedError
+
 
 class InputPort(AbstractPort):
     """Input port.
-    TODO: More info.
+
+    May have one or zero signals connected to it.
     """
 
     _source_signal: Optional[Signal]
 
-    def __init__(self, port_id: PortIndex, operation: Operation):
-        super().__init__(port_id, operation)
+    def __init__(self, operation: "Operation", index: int):
+        """Construct an InputPort."""
+        super().__init__(operation, index)
         self._source_signal = None
 
     @property
-    def signals(self) -> List[Signal]:
-        return [] if self._source_signal is None else [self._source_signal]
-
-    def signal(self, i: int = 0) -> Signal:
-        assert 0 <= i < self.signal_count(), "Signal index out of bound."
-        assert self._source_signal is not None, "No Signal connect to InputPort."
-        return self._source_signal
-
-    @property
-    def connected_ports(self) -> List[Port]:
-        return [] if self._source_signal is None or self._source_signal.source is None \
-            else [self._source_signal.source]
-
     def signal_count(self) -> int:
         return 0 if self._source_signal is None else 1
 
-    def connect(self, port: "OutputPort") -> Signal:
-        assert self._source_signal is None, "Connecting new port to already connected input port."
-        return Signal(port, self)        # self._source_signal is set by the signal constructor
+    @property
+    def signals(self) -> Iterable[Signal]:
+        return [] if self._source_signal is None else [self._source_signal]
 
     def add_signal(self, signal: Signal) -> None:
-        assert self._source_signal is None, "Connecting new port to already connected input port."
-        self._source_signal: Signal = signal
-        if self is not signal.destination:
-            # Connect this inputport as destination for this signal if it isn't already.
-            signal.set_destination(self)
-
-    def disconnect(self, port: "OutputPort") -> None:
-        assert self._source_signal.source is port, "The entered port is not connected to this port."
-        self._source_signal.remove_source()
+        assert self._source_signal is None, "Input port may have only one signal added."
+        assert signal is not self._source_signal, "Attempted to add already connected signal."
+        self._source_signal = signal
+        signal.set_destination(self)
 
     def remove_signal(self, signal: Signal) -> None:
-        old_signal: Signal = self._source_signal
+        assert signal is self._source_signal, "Attempted to remove signal that is not connected."
         self._source_signal = None
-        if self is old_signal.destination:
-            # Disconnect the dest of the signal if this inputport currently is the dest
-            old_signal.remove_destination()
+        signal.remove_destination()
 
     def clear(self) -> None:
-        self.remove_signal(self._source_signal)
+        if self._source_signal is not None:
+            self.remove_signal(self._source_signal)
+
+    @property
+    def connected_source(self) -> Optional["OutputPort"]:
+        """Get the output port that is currently connected to this input port,
+        or None if it is unconnected.
+        """
+        return None if self._source_signal is None else self._source_signal.source
+
+    def connect(self, src: SignalSourceProvider, name: Name = "") -> Signal:
+        """Connect the provided signal source to this input port by creating a new signal.
+        Returns the new signal.
+        """
+        assert self._source_signal is None, "Attempted to connect already connected input port."
+        # self._source_signal is set by the signal constructor.
+        return Signal(source=src.source, destination=self, name=name)
+
+    def __lshift__(self, src: SignalSourceProvider) -> Signal:
+        """Overloads the left shift operator to make it connect the provided signal source to this input port.
+        Returns the new signal.
+        """
+        return self.connect(src)
+
 
-class OutputPort(AbstractPort):
+class OutputPort(AbstractPort, SignalSourceProvider):
     """Output port.
-    TODO: More info.
+
+    May have zero or more signals connected to it.
     """
 
     _destination_signals: List[Signal]
 
-    def __init__(self, port_id: PortIndex, operation: Operation):
-        super().__init__(port_id, operation)
+    def __init__(self, operation: "Operation", index: int):
+        """Construct an OutputPort."""
+        super().__init__(operation, index)
         self._destination_signals = []
 
     @property
-    def signals(self) -> List[Signal]:
-        return self._destination_signals.copy()
-
-    def signal(self, i: int = 0) -> Signal:
-        assert 0 <= i < self.signal_count(), "Signal index out of bounds."
-        return self._destination_signals[i]
-
-    @property
-    def connected_ports(self) -> List[Port]:
-        return [signal.destination for signal in self._destination_signals \
-            if signal.destination is not None]
-
     def signal_count(self) -> int:
         return len(self._destination_signals)
 
-    def connect(self, port: InputPort) -> Signal:
-        return Signal(self, port)      # Signal is added to self._destination_signals in signal constructor
+    @property
+    def signals(self) -> Iterable[Signal]:
+        return self._destination_signals
 
     def add_signal(self, signal: Signal) -> None:
-        assert signal not in self.signals, \
-                "Attempting to connect to Signal already connected."
+        assert signal not in self._destination_signals, "Attempted to add already connected signal."
         self._destination_signals.append(signal)
-        if self is not signal.source:
-            # Connect this outputport to the signal if it isn't already
-            signal.set_source(self)
-
-    def disconnect(self, port: InputPort) -> None:
-        assert port in self.connected_ports, "Attempting to disconnect port that isn't connected."
-        for sig in self._destination_signals:
-            if sig.destination is port:
-                sig.remove_destination()
-                break
+        signal.set_source(self)
 
     def remove_signal(self, signal: Signal) -> None:
-        i: int = self._destination_signals.index(signal)
-        old_signal: Signal = self._destination_signals[i]
-        del self._destination_signals[i]
-        if self is old_signal.source:
-            old_signal.remove_source()
+        assert signal in self._destination_signals, "Attempted to remove signal that is not connected."
+        self._destination_signals.remove(signal)
+        signal.remove_source()
 
     def clear(self) -> None:
-        for signal in self._destination_signals:
+        for signal in copy(self._destination_signals):
             self.remove_signal(signal)
+
+    @property
+    def source(self) -> "OutputPort":
+        return self
diff --git a/b_asic/precedence_chart.py b/b_asic/precedence_chart.py
deleted file mode 100644
index be55a123e0ab4330057c0bb62581e45195f5e5ba..0000000000000000000000000000000000000000
--- a/b_asic/precedence_chart.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""@package docstring
-B-ASIC Precedence Chart Module.
-TODO: More info.
-"""
-
-from b_asic.signal_flow_graph import SFG
-
-
-class PrecedenceChart:
-    """Precedence chart constructed from a signal flow graph.
-    TODO: More info.
-    """
-
-    sfg: SFG
-    # TODO: More members.
-
-    def __init__(self, sfg: SFG):
-        self.sfg = sfg
-        # TODO: Implement.
-
-    # TODO: More stuff.
diff --git a/b_asic/save_load_structure.py b/b_asic/save_load_structure.py
new file mode 100644
index 0000000000000000000000000000000000000000..222e474193691ce9b1ca9b7320dc5ffd6fd9b75d
--- /dev/null
+++ b/b_asic/save_load_structure.py
@@ -0,0 +1,90 @@
+"""B-ASIC Save/Load Structure Module.
+
+Contains functions for saving/loading SFGs to/from strings that can be stored
+as files.
+"""
+
+from b_asic.signal_flow_graph import SFG
+from b_asic.graph_component import GraphComponent
+
+from datetime import datetime
+from inspect import signature
+from os import path
+
+
+def sfg_to_python(sfg: SFG, counter: int = 0, suffix: str = None) -> str:
+    """Given an SFG structure try to serialize it for saving to a file."""
+    result = (
+        "\n\"\"\"\nB-ASIC automatically generated SFG file.\n" +
+        "Name: " + f"{sfg.name}" + "\n" +
+        "Last saved: " + f"{datetime.now()}" + ".\n" +
+        "\"\"\""
+    )
+
+    result += "\nfrom b_asic import SFG, Signal, Input, Output"
+    for op in {type(op) for op in sfg.operations}:
+        result += f", {op.__name__}"
+
+    def kwarg_unpacker(comp: GraphComponent, params=None) -> str:
+        if params is None:
+            params_filtered = {attr: getattr(op, attr) for attr in signature(
+                op.__init__).parameters if attr != "latency" and hasattr(op, attr)}
+            params = {attr: getattr(op, attr) if not isinstance(getattr(
+                op, attr), str) else f'"{getattr(op, attr)}"' for attr in params_filtered}
+
+        return ", ".join([f"{param[0]}={param[1]}" for param in params.items()])
+
+    result += "\n# Inputs:\n"
+    for op in sfg._input_operations:
+        result += f"{op.graph_id} = Input({kwarg_unpacker(op)})\n"
+
+    result += "\n# Outputs:\n"
+    for op in sfg._output_operations:
+        result += f"{op.graph_id} = Output({kwarg_unpacker(op)})\n"
+
+    result += "\n# Operations:\n"
+    for op in sfg.split():
+        if isinstance(op, SFG):
+            counter += 1
+            result = sfg_to_python(op, counter) + result
+            continue
+
+        result += f"{op.graph_id} = {op.__class__.__name__}({kwarg_unpacker(op)})\n"
+
+    result += "\n# Signals:\n"
+    # Keep track of already existing connections to avoid adding duplicates
+    connections = list()
+    for op in sfg.split():
+        for out in op.outputs:
+            for signal in out.signals:
+                dest_op = signal.destination.operation
+                connection = f"\nSignal(source={op.graph_id}.output({op.outputs.index(signal.source)}), destination={dest_op.graph_id}.input({dest_op.inputs.index(signal.destination)}))"
+                if connection in connections:
+                    continue
+
+                result += connection
+                connections.append(connection)
+
+    inputs = "[" + ", ".join(op.graph_id for op in sfg.input_operations) + "]"
+    outputs = "[" + \
+        ", ".join(op.graph_id for op in sfg.output_operations) + "]"
+    sfg_name = sfg.name if sfg.name else "sfg" + \
+        str(counter) if counter > 0 else 'sfg'
+    sfg_name_var = sfg_name.replace(" ", "_")
+    result += f"\n{sfg_name_var} = SFG(inputs={inputs}, outputs={outputs}, name='{sfg_name}')\n"
+    result += "\n# SFG Properties:\n" + \
+        "prop = {'name':" + f"{sfg_name_var}" + "}"
+
+    if suffix is not None:
+        result += "\n" + suffix + "\n"
+
+    return result
+
+
+def python_to_sfg(path: str) -> SFG:
+    """Given a serialized file try to deserialize it and load it to the library."""
+    with open(path) as f:
+        code = compile(f.read(), path, 'exec')
+        exec(code, globals(), locals())
+
+    return locals()["prop"]["name"], locals()["positions"] if "positions" in locals() else {}
diff --git a/b_asic/schema.py b/b_asic/schema.py
index e5068cdc080c5c5004c44c885ac48f52ba44c1f3..25f18eb6fb423009edd52f3d2f40024fb37e3422 100644
--- a/b_asic/schema.py
+++ b/b_asic/schema.py
@@ -1,21 +1,112 @@
-"""@package docstring
-B-ASIC Schema Module.
-TODO: More info.
+"""B-ASIC Schema Module.
+
+Contains the schema class for scheduling operations in an SFG.
 """
 
-from b_asic.precedence_chart import PrecedenceChart
+from typing import Dict, List, Optional
+
+from b_asic.signal_flow_graph import SFG
+from b_asic.graph_component import GraphID
+from b_asic.operation import Operation
 
 
 class Schema:
-    """Schema constructed from a precedence chart.
-    TODO: More info.
-    """
+    """Schema of an SFG with scheduled Operations."""
+
+    _sfg: SFG
+    _start_times: Dict[GraphID, int]
+    _laps: Dict[GraphID, List[int]]
+    _schedule_time: int
+    _cyclic: bool
+    _resolution: int
+
+    def __init__(self, sfg: SFG, schedule_time: Optional[int] = None, cyclic: bool = False, resolution: int = 1, scheduling_alg: str = "ASAP"):
+        """Construct a Schema from an SFG."""
+        self._sfg = sfg
+        self._start_times = dict()
+        self._laps = dict()
+        self._cyclic = cyclic
+        self._resolution = resolution
+
+        if scheduling_alg == "ASAP":
+            self._schedule_asap()
+        else:
+            raise NotImplementedError(
+                f"No algorithm with name: {scheduling_alg} defined.")
+
+        max_end_time = 0
+        for op_id, op_start_time in self._start_times.items():
+            op = self._sfg.find_by_id(op_id)
+            for outport in op.outputs:
+                max_end_time = max(
+                    max_end_time, op_start_time + outport.latency_offset)
+
+        if not self._cyclic:
+            if schedule_time is None:
+                self._schedule_time = max_end_time
+            elif schedule_time < max_end_time:
+                raise ValueError(
+                    "Too short schedule time for non-cyclic Schedule entered.")
+            else:
+                self._schedule_time = schedule_time
+
+    def start_time_of_operation(self, op_id: GraphID) -> int:
+        """Get the start time of the operation with the specified by the op_id."""
+        assert op_id in self._start_times, "No operation with the specified op_id in this schema."
+        return self._start_times[op_id]
+
+    def forward_slack(self, op_id: GraphID) -> int:
+        raise NotImplementedError
+
+    def backward_slack(self, op_id: GraphID) -> int:
+        raise NotImplementedError
+
+    def print_slacks(self) -> None:
+        raise NotImplementedError
+
+    def _schedule_asap(self) -> None:
+        pl = self._sfg.get_precedence_list()
+
+        if len(pl) < 2:
+            print("Empty signal flow graph cannot be scheduled.")
+            return
+
+        non_schedulable_ops = set((outp.operation.graph_id for outp in pl[0]))
+
+        for outport in pl[1]:
+            op = outport.operation
+            if op not in self._start_times:
+                # Set start time of all operations in the first iter to 0
+                self._start_times[op.graph_id] = 0
+
+        for outports in pl[2:]:
+            for outport in outports:
+                op = outport.operation
+                if op.graph_id not in self._start_times:
+                    # Schedule the operation if it doesn't have a start time yet.
+                    op_start_time = 0
+                    for inport in op.inputs:
+                        print(inport.operation.graph_id)
+                        assert len(
+                            inport.signals) == 1, "Error in scheduling, dangling input port detected."
+                        assert inport.signals[0].source is not None, "Error in scheduling, signal with no source detected."
+                        source_port = inport.signals[0].source
+
+                        source_end_time = None
+                        if source_port.operation.graph_id in non_schedulable_ops:
+                            source_end_time = 0
+                        else:
+                            source_op_time = self._start_times[source_port.operation.graph_id]
+
+                            assert source_port.latency_offset is not None, f"Output port: {source_port.index} of operation: \
+                                    {source_port.operation.graph_id} has no latency-offset."
+                            assert inport.latency_offset is not None, f"Input port: {inport.index} of operation: \
+                                    {inport.operation.graph_id} has no latency-offset."
 
-    pc: PrecedenceChart
-    # TODO: More members.
+                            source_end_time = source_op_time + source_port.latency_offset
 
-    def __init__(self, pc: PrecedenceChart):
-        self.pc = pc
-        # TODO: Implement.
+                        op_start_time_from_in = source_end_time - inport.latency_offset
+                        op_start_time = max(
+                            op_start_time, op_start_time_from_in)
 
-    # TODO: More stuff.
+                    self._start_times[op.graph_id] = op_start_time
diff --git a/b_asic/signal.py b/b_asic/signal.py
index 64c259486abd78e3b18ea824b58bfabe271f50d8..747b25fc9c51c1cde707066e42778514b80f9eb6 100644
--- a/b_asic/signal.py
+++ b/b_asic/signal.py
@@ -1,9 +1,10 @@
-"""@package docstring
-B-ASIC Signal Module.
+"""B-ASIC Signal Module.
+
+Contains the class for representing the connections between operations.
 """
-from typing import Optional, TYPE_CHECKING
+from typing import Optional, Iterable, TYPE_CHECKING
 
-from b_asic.graph_component import AbstractGraphComponent, TypeName, Name
+from b_asic.graph_component import GraphComponent, AbstractGraphComponent, TypeName, Name
 
 if TYPE_CHECKING:
     from b_asic.port import InputPort, OutputPort
@@ -12,30 +13,35 @@ if TYPE_CHECKING:
 class Signal(AbstractGraphComponent):
     """A connection between two ports."""
 
-    _source: "OutputPort"
-    _destination: "InputPort"
-
-    def __init__(self, source: Optional["OutputPort"] = None, \
-                destination: Optional["InputPort"] = None, name: Name = ""):
+    _source: Optional["OutputPort"]
+    _destination: Optional["InputPort"]
 
+    def __init__(self, source: Optional["OutputPort"] = None, destination: Optional["InputPort"] = None, bits: Optional[int] = None, name: Name = ""):
+        """Construct a Signal."""
         super().__init__(name)
-
-        self._source = source
-        self._destination = destination
-
+        self._source = None
+        self._destination = None
         if source is not None:
             self.set_source(source)
-
         if destination is not None:
             self.set_destination(destination)
+        self.set_param("bits", bits)
+
+    @classmethod
+    def type_name(cls) -> TypeName:
+        return "s"
 
     @property
-    def source(self) -> "OutputPort":
+    def neighbors(self) -> Iterable[GraphComponent]:
+        return [p.operation for p in [self.source, self.destination] if p is not None]
+
+    @property
+    def source(self) -> Optional["OutputPort"]:
         """Return the source OutputPort of the signal."""
         return self._source
 
     @property
-    def destination(self) -> "InputPort":
+    def destination(self) -> Optional["InputPort"]:
         """Return the destination "InputPort" of the signal."""
         return self._destination
 
@@ -47,11 +53,11 @@ class Signal(AbstractGraphComponent):
         Keyword arguments:
         - src: OutputPort to connect as source to the signal.
         """
-        self.remove_source()
-        self._source = src
-        if self not in src.signals:
-            # If the new source isn't connected to this signal then connect it.
-            src.add_signal(self)
+        if src is not self._source:
+            self.remove_source()
+            self._source = src
+            if self not in src.signals:
+                src.add_signal(self)
 
     def set_destination(self, dest: "InputPort") -> None:
         """Disconnect the previous destination InputPort of the signal and
@@ -61,36 +67,44 @@ class Signal(AbstractGraphComponent):
         Keywords argments:
         - dest: InputPort to connect as destination to the signal.
         """
-        self.remove_destination()
-        self._destination = dest
-        if self not in dest.signals:
-            # If the new destination isn't connected to tis signal then connect it.
-            dest.add_signal(self)
-
-    @property
-    def type_name(self) -> TypeName:
-        return "s"
+        if dest is not self._destination:
+            self.remove_destination()
+            self._destination = dest
+            if self not in dest.signals:
+                dest.add_signal(self)
 
     def remove_source(self) -> None:
         """Disconnect the source OutputPort of the signal. If the source port
         still is connected to this signal then also disconnect the source port."""
-        if self._source is not None:
-            old_source: "OutputPort" = self._source
+        src = self._source
+        if src is not None:
             self._source = None
-            if self in old_source.signals:
-                # If the old destination port still is connected to this signal, then disconnect it.
-                old_source.remove_signal(self)
+            if self in src.signals:
+                src.remove_signal(self)
 
     def remove_destination(self) -> None:
         """Disconnect the destination InputPort of the signal."""
-        if self._destination is not None:
-            old_destination: "InputPort" = self._destination
+        dest = self._destination
+        if dest is not None:
             self._destination = None
-            if self in old_destination.signals:
-                # If the old destination port still is connected to this signal, then disconnect it.
-                old_destination.remove_signal(self)
+            if self in dest.signals:
+                dest.remove_signal(self)
 
-    def is_connected(self) -> bool:
-        """Returns true if the signal is connected to both a source and a destination,
+    def dangling(self) -> bool:
+        """Returns true if the signal is missing either a source or a destination,
         else false."""
-        return self._source is not None and self._destination is not None
+        return self._source is None or self._destination is None
+
+    @property
+    def bits(self) -> Optional[int]:
+        """Get the number of bits that this operations using this signal as an input should truncate received values to.
+        None = unlimited."""
+        return self.param("bits")
+
+    @bits.setter
+    def bits(self, bits: Optional[int]) -> None:
+        """Set the number of bits that operations using this signal as an input should truncate received values to.
+        None = unlimited."""
+        assert bits is None or (isinstance(bits, int)
+                                and bits >= 0), "Bits must be non-negative."
+        self.set_param("bits", bits)
diff --git a/b_asic/signal_flow_graph.py b/b_asic/signal_flow_graph.py
index 9c08aecc40ff77b8fe90051b6ea165c0f1703b9b..1c0f48708eaaa761ffc822f46e24c3d88df04eb9 100644
--- a/b_asic/signal_flow_graph.py
+++ b/b_asic/signal_flow_graph.py
@@ -1,91 +1,833 @@
-"""@package docstring
-B-ASIC Signal Flow Graph Module.
-TODO: More info.
+"""B-ASIC Signal Flow Graph Module.
+
+Contains the signal flow graph operation.
 """
 
-from typing import List, Dict, Optional, DefaultDict
-from collections import defaultdict
+from typing import List, Iterable, Sequence, Dict, Optional, DefaultDict, MutableSet, Tuple
+from numbers import Number
+from collections import defaultdict, deque
+from io import StringIO
+from queue import PriorityQueue
+import itertools as it
+from graphviz import Digraph
 
-from b_asic.operation import Operation
-from b_asic.operation import AbstractOperation
+from b_asic.port import SignalSourceProvider, OutputPort
+from b_asic.operation import Operation, AbstractOperation, ResultKey, DelayMap, MutableResultMap, MutableDelayMap
 from b_asic.signal import Signal
-from b_asic.graph_id import GraphIDGenerator, GraphID
-from b_asic.graph_component import GraphComponent, Name, TypeName
+from b_asic.graph_component import GraphID, GraphIDNumber, GraphComponent, Name, TypeName
+from b_asic.special_operations import Input, Output, Delay
+
+
+DelayQueue = List[Tuple[str, ResultKey, OutputPort]]
+
+
+class GraphIDGenerator:
+    """Generates Graph IDs for objects."""
+
+    _next_id_number: DefaultDict[TypeName, GraphIDNumber]
+
+    def __init__(self, id_number_offset: GraphIDNumber = 0):
+        """Construct a GraphIDGenerator."""
+        self._next_id_number = defaultdict(lambda: id_number_offset)
+
+    def next_id(self, type_name: TypeName) -> GraphID:
+        """Get the next graph id for a certain graph id type."""
+        self._next_id_number[type_name] += 1
+        return type_name + str(self._next_id_number[type_name])
+
+    @property
+    def id_number_offset(self) -> GraphIDNumber:
+        """Get the graph id number offset of this generator."""
+        return self._next_id_number.default_factory()  # pylint: disable=not-callable
 
 
 class SFG(AbstractOperation):
     """Signal flow graph.
-    TODO: More info.
+
+    Contains a set of connected operations, forming a new operation.
+    Used as a base for simulation, scheduling, etc.
     """
 
-    _graph_components_by_id: Dict[GraphID, GraphComponent]
-    _graph_components_by_name: DefaultDict[Name, List[GraphComponent]]
+    _components_by_id: Dict[GraphID, GraphComponent]
+    _components_by_name: DefaultDict[Name, List[GraphComponent]]
+    _components_dfs_order: List[GraphComponent]
+    _operations_dfs_order: List[Operation]
+    _operations_topological_order: List[Operation]
     _graph_id_generator: GraphIDGenerator
+    _input_operations: List[Input]
+    _output_operations: List[Output]
+    _original_components_to_new: MutableSet[GraphComponent]
+    _original_input_signals_to_indices: Dict[Signal, int]
+    _original_output_signals_to_indices: Dict[Signal, int]
+    _precedence_list: Optional[List[List[OutputPort]]]
+
+    def __init__(self, inputs: Optional[Sequence[Input]] = None, outputs: Optional[Sequence[Output]] = None,
+                 input_signals: Optional[Sequence[Signal]] = None, output_signals: Optional[Sequence[Signal]] = None,
+                 id_number_offset: GraphIDNumber = 0, name: Name = "",
+                 input_sources: Optional[Sequence[Optional[SignalSourceProvider]]] = None):
+        """Construct an SFG given its inputs and outputs.
+
+        Inputs/outputs may be specified using either Input/Output operations
+        directly with the inputs/outputs parameters, or using signals with the
+        input_signals/output_signals parameters. If signals are used, the
+        corresponding Input/Output operations will be created automatically.
+
+        The id_number_offset parameter specifies what number graph IDs will be
+        offset by for each new graph component type. IDs start at 1 by default,
+        so the default offset of 0 will result in IDs like "c1", "c2", etc.
+        while an offset of 3 will result in "c4", "c5", etc.
+        """
+
+        input_signal_count = 0 if input_signals is None else len(input_signals)
+        input_operation_count = 0 if inputs is None else len(inputs)
+        output_signal_count = 0 if output_signals is None else len(
+            output_signals)
+        output_operation_count = 0 if outputs is None else len(outputs)
+        super().__init__(input_count=input_signal_count + input_operation_count,
+                         output_count=output_signal_count + output_operation_count,
+                         name=name, input_sources=input_sources)
+
+        self._components_by_id = dict()
+        self._components_by_name = defaultdict(list)
+        self._components_dfs_order = []
+        self._operations_dfs_order = []
+        self._operations_topological_order = []
+        self._graph_id_generator = GraphIDGenerator(id_number_offset)
+        self._input_operations = []
+        self._output_operations = []
+        self._original_components_to_new = {}
+        self._original_input_signals_to_indices = {}
+        self._original_output_signals_to_indices = {}
+        self._precedence_list = None
+
+        # Setup input signals.
+        if input_signals is not None:
+            for input_index, signal in enumerate(input_signals):
+                assert signal not in self._original_components_to_new, "Duplicate input signals supplied to SFG construcctor."
+                new_input_op = self._add_component_unconnected_copy(Input())
+                new_signal = self._add_component_unconnected_copy(signal)
+                new_signal.set_source(new_input_op.output(0))
+                self._input_operations.append(new_input_op)
+                self._original_input_signals_to_indices[signal] = input_index
 
-    def __init__(self, input_signals: List[Signal] = None, output_signals: List[Signal] = None, \
-                ops: List[Operation] = None, **kwds):
-        super().__init__(**kwds)
-        if input_signals is None:
-            input_signals = []
-        if output_signals is None:
-            output_signals = []
-        if ops is None:
-            ops = []
+        # Setup input operations, starting from indices ater input signals.
+        if inputs is not None:
+            for input_index, input_op in enumerate(inputs, input_signal_count):
+                assert input_op not in self._original_components_to_new, "Duplicate input operations supplied to SFG constructor."
+                new_input_op = self._add_component_unconnected_copy(input_op)
+                for signal in input_op.output(0).signals:
+                    assert signal not in self._original_components_to_new, "Duplicate input signals connected to input ports supplied to SFG construcctor."
+                    new_signal = self._add_component_unconnected_copy(signal)
+                    new_signal.set_source(new_input_op.output(0))
+                    self._original_input_signals_to_indices[signal] = input_index
 
-        self._graph_components_by_id = dict() # Maps Graph ID to objects
-        self._graph_components_by_name = defaultdict(list) # Maps Name to objects
-        self._graph_id_generator = GraphIDGenerator()
+                self._input_operations.append(new_input_op)
 
-        for operation in ops:
-            self._add_graph_component(operation)
+        # Setup output signals.
+        if output_signals is not None:
+            for output_index, signal in enumerate(output_signals):
+                new_output_op = self._add_component_unconnected_copy(Output())
+                if signal in self._original_components_to_new:
+                    # Signal was already added when setting up inputs.
+                    new_signal = self._original_components_to_new[signal]
+                    new_signal.set_destination(new_output_op.input(0))
+                else:
+                    # New signal has to be created.
+                    new_signal = self._add_component_unconnected_copy(signal)
+                    new_signal.set_destination(new_output_op.input(0))
 
-        for input_signal in input_signals:
-            self._add_graph_component(input_signal)
+                self._output_operations.append(new_output_op)
+                self._original_output_signals_to_indices[signal] = output_index
 
-        # TODO: Construct SFG based on what inputs that were given
-        # TODO: Traverse the graph between the inputs/outputs and add to self._operations.
-        # TODO: Connect ports with signals with appropriate IDs.
+        # Setup output operations, starting from indices after output signals.
+        if outputs is not None:
+            for output_index, output_op in enumerate(outputs, output_signal_count):
+                assert output_op not in self._original_components_to_new, "Duplicate output operations supplied to SFG constructor."
+                new_output_op = self._add_component_unconnected_copy(output_op)
+                for signal in output_op.input(0).signals:
+                    new_signal = None
+                    if signal in self._original_components_to_new:
+                        # Signal was already added when setting up inputs.
+                        new_signal = self._original_components_to_new[signal]
+                    else:
+                        # New signal has to be created.
+                        new_signal = self._add_component_unconnected_copy(
+                            signal)
 
-    def evaluate(self, *inputs) -> list:
-        return [] # TODO: Implement
+                    new_signal.set_destination(new_output_op.input(0))
+                    self._original_output_signals_to_indices[signal] = output_index
 
-    def _add_graph_component(self, graph_component: GraphComponent) -> GraphID:
-        """Add the entered graph component to the SFG's dictionary of graph objects and
-         return a generated GraphID for it.
+                self._output_operations.append(new_output_op)
+
+        output_operations_set = set(self._output_operations)
+
+        # Search the graph inwards from each input signal.
+        for signal, input_index in self._original_input_signals_to_indices.items():
+            # Check if already added destination.
+            new_signal = self._original_components_to_new[signal]
+            if new_signal.destination is None:
+                if signal.destination is None:
+                    raise ValueError(
+                        f"Input signal #{input_index} is missing destination in SFG")
+                if signal.destination.operation not in self._original_components_to_new:
+                    self._add_operation_connected_tree_copy(
+                        signal.destination.operation)
+            elif new_signal.destination.operation in output_operations_set:
+                # Add directly connected input to output to ordered list.
+                self._components_dfs_order.extend(
+                    [new_signal.source.operation, new_signal, new_signal.destination.operation])
+                self._operations_dfs_order.extend(
+                    [new_signal.source.operation, new_signal.destination.operation])
+
+        # Search the graph inwards from each output signal.
+        for signal, output_index in self._original_output_signals_to_indices.items():
+            # Check if already added source.
+            new_signal = self._original_components_to_new[signal]
+            if new_signal.source is None:
+                if signal.source is None:
+                    raise ValueError(
+                        f"Output signal #{output_index} is missing source in SFG")
+                if signal.source.operation not in self._original_components_to_new:
+                    self._add_operation_connected_tree_copy(
+                        signal.source.operation)
+
+    def __str__(self) -> str:
+        """Get a string representation of this SFG."""
+        string_io = StringIO()
+        string_io.write(super().__str__() + "\n")
+        string_io.write("Internal Operations:\n")
+        line = "-" * 100 + "\n"
+        string_io.write(line)
+
+        for operation in self.get_operations_topological_order():
+            string_io.write(str(operation) + "\n")
+
+        string_io.write(line)
+
+        return string_io.getvalue()
+
+    def __call__(self, *src: Optional[SignalSourceProvider], name: Name = "") -> "SFG":
+        """Get a new independent SFG instance that is identical to this SFG except without any of its external connections."""
+        return SFG(inputs=self._input_operations, outputs=self._output_operations,
+                   id_number_offset=self.id_number_offset, name=name, input_sources=src if src else None)
+
+    @classmethod
+    def type_name(cls) -> TypeName:
+        return "sfg"
+
+    def evaluate(self, *args):
+        result = self.evaluate_outputs(args)
+        n = len(result)
+        return None if n == 0 else result[0] if n == 1 else result
+
+    def evaluate_output(self, index: int, input_values: Sequence[Number], results: Optional[MutableResultMap] = None, delays: Optional[MutableDelayMap] = None, prefix: str = "", bits_override: Optional[int] = None, truncate: bool = True) -> Number:
+        if index < 0 or index >= self.output_count:
+            raise IndexError(
+                f"Output index out of range (expected 0-{self.output_count - 1}, got {index})")
+        if len(input_values) != self.input_count:
+            raise ValueError(
+                f"Wrong number of inputs supplied to SFG for evaluation (expected {self.input_count}, got {len(input_values)})")
+        if results is None:
+            results = {}
+        if delays is None:
+            delays = {}
+
+        # Set the values of our input operations to the given input values.
+        for op, arg in zip(self._input_operations, self.truncate_inputs(input_values, bits_override) if truncate else input_values):
+            op.value = arg
+
+        deferred_delays = []
+        value = self._evaluate_source(self._output_operations[index].input(
+            0).signals[0].source, results, delays, prefix, bits_override, truncate, deferred_delays)
+        while deferred_delays:
+            new_deferred_delays = []
+            for key_base, key, src in deferred_delays:
+                self._do_evaluate_source(
+                    key_base, key, src, results, delays, prefix, bits_override, truncate, new_deferred_delays)
+            deferred_delays = new_deferred_delays
+        results[self.key(index, prefix)] = value
+        return value
+
+    def connect_external_signals_to_components(self) -> bool:
+        """ Connects any external signals to this SFG's internal operations. This SFG becomes unconnected to the SFG
+        it is a component off, causing it to become invalid afterwards. Returns True if succesful, False otherwise. """
+        if len(self.inputs) != len(self.input_operations):
+            raise IndexError(
+                f"Number of inputs does not match the number of input_operations in SFG.")
+        if len(self.outputs) != len(self.output_operations):
+            raise IndexError(
+                f"Number of outputs does not match the number of output_operations SFG.")
+        if len(self.input_signals) == 0:
+            return False
+        if len(self.output_signals) == 0:
+            return False
+
+        # For each input_signal, connect it to the corresponding operation
+        for port, input_operation in zip(self.inputs, self.input_operations):
+            dest = input_operation.output(0).signals[0].destination
+            dest.clear()
+            port.signals[0].set_destination(dest)
+        # For each output_signal, connect it to the corresponding operation
+        for port, output_operation in zip(self.outputs, self.output_operations):
+            src = output_operation.input(0).signals[0].source
+            src.clear()
+            port.signals[0].set_source(src)
+        return True
+
+    @property
+    def input_operations(self) -> Sequence[Operation]:
+        """Get the internal input operations in the same order as their respective input ports."""
+        return self._input_operations
+
+    @property
+    def output_operations(self) -> Sequence[Operation]:
+        """Get the internal output operations in the same order as their respective output ports."""
+        return self._output_operations
+
+    def split(self) -> Iterable[Operation]:
+        return self.operations
+
+    def to_sfg(self) -> 'SFG':
+        return self
+
+    def inputs_required_for_output(self, output_index: int) -> Iterable[int]:
+        if output_index < 0 or output_index >= self.output_count:
+            raise IndexError(
+                f"Output index out of range (expected 0-{self.output_count - 1}, got {output_index})")
+
+        input_indexes_required = []
+        sfg_input_operations_to_indexes = {
+            input_op: index for index, input_op in enumerate(self._input_operations)}
+        output_op = self._output_operations[output_index]
+        queue = deque([output_op])
+        visited = set([output_op])
+        while queue:
+            op = queue.popleft()
+            if isinstance(op, Input):
+                if op in sfg_input_operations_to_indexes:
+                    input_indexes_required.append(
+                        sfg_input_operations_to_indexes[op])
+                    del sfg_input_operations_to_indexes[op]
+
+            for input_port in op.inputs:
+                for signal in input_port.signals:
+                    if signal.source is not None:
+                        new_op = signal.source.operation
+                        if new_op not in visited:
+                            queue.append(new_op)
+                            visited.add(new_op)
+
+        return input_indexes_required
+
+    def copy_component(self, *args, **kwargs) -> GraphComponent:
+        return super().copy_component(*args, **kwargs, inputs=self._input_operations, outputs=self._output_operations,
+                                      id_number_offset=self.id_number_offset, name=self.name)
+
+    @property
+    def id_number_offset(self) -> GraphIDNumber:
+        """Get the graph id number offset of the graph id generator for this SFG."""
+        return self._graph_id_generator.id_number_offset
+
+    @property
+    def components(self) -> Iterable[GraphComponent]:
+        """Get all components of this graph in depth-first order."""
+        return self._components_dfs_order
+
+    @property
+    def operations(self) -> Iterable[Operation]:
+        """Get all operations of this graph in depth-first order."""
+        return self._operations_dfs_order
+
+    def find_by_type_name(self, type_name: TypeName) -> Sequence[GraphComponent]:
+        """Find all components in this graph with the specified type name.
+        Returns an empty sequence if no components were found.
 
         Keyword arguments:
-        graph_component: Graph component to add to the graph.
+        type_name: The type_name of the desired components.
         """
-        # Add to name dict
-        self._graph_components_by_name[graph_component.name].append(graph_component)
+        i = self.id_number_offset + 1
+        components = []
+        found_comp = self.find_by_id(type_name + str(i))
+        while found_comp is not None:
+            components.append(found_comp)
+            i += 1
+            found_comp = self.find_by_id(type_name + str(i))
 
-        # Add to ID dict
-        graph_id: GraphID = self._graph_id_generator.get_next_id(graph_component.type_name)
-        self._graph_components_by_id[graph_id] = graph_component
-        return graph_id
+        return components
 
     def find_by_id(self, graph_id: GraphID) -> Optional[GraphComponent]:
-        """Find a graph object based on the entered Graph ID and return it. If no graph
-        object with the entered ID was found then return None.
+        """Find the graph component with the specified ID.
+        Returns None if the component was not found.
 
         Keyword arguments:
-        graph_id: Graph ID of the wanted object.
+        graph_id: Graph ID of the desired component.
         """
-        if graph_id in self._graph_components_by_id:
-            return self._graph_components_by_id[graph_id]
+        return self._components_by_id.get(graph_id, None)
 
-        return None
+    def find_by_name(self, name: Name) -> Sequence[GraphComponent]:
+        """Find all graph components with the specified name.
+        Returns an empty sequence if no components were found.
 
-    def find_by_name(self, name: Name) -> List[GraphComponent]:
-        """Find all graph objects that have the entered name and return them
-        in a list. If no graph object with the entered name was found then return an
-        empty list.
+        Keyword arguments:
+        name: Name of the desired component(s)
+        """
+        return self._components_by_name.get(name, [])
+
+    def find_result_keys_by_name(self, name: Name, output_index: int = 0) -> Sequence[ResultKey]:
+        """Find all graph components with the specified name and
+        return a sequence of the keys to use when fetching their results
+        from a simulation.
 
         Keyword arguments:
-        name: Name of the wanted object.
+        name: Name of the desired component(s)
+        output_index: The desired output index to get the result from
         """
-        return self._graph_components_by_name[name]
+        keys = []
+        for comp in self.find_by_name(name):
+            if isinstance(comp, Operation):
+                keys.append(comp.key(output_index, comp.graph_id))
+        return keys
 
-    @property
-    def type_name(self) -> TypeName:
-        return "sfg"
+    def replace_component(self, component: Operation, graph_id: GraphID) -> "SFG":
+        """Find and replace all components matching either on GraphID, Type or both.
+        Then return a new deepcopy of the sfg with the replaced component.
+
+        Arguments:
+        component: The new component(s), e.g Multiplication
+        graph_id: The GraphID to match the component to replace.
+        """
+
+        sfg_copy = self()  # Copy to not mess with this SFG.
+        component_copy = sfg_copy.find_by_id(graph_id)
+
+        assert component_copy is not None and isinstance(component_copy, Operation), \
+            "No operation matching the criteria found"
+        assert component_copy.output_count == component.output_count, \
+            "The output count may not differ between the operations"
+        assert component_copy.input_count == component.input_count, \
+            "The input count may not differ between the operations"
+
+        for index_in, inp in enumerate(component_copy.inputs):
+            for signal in inp.signals:
+                signal.remove_destination()
+                signal.set_destination(component.input(index_in))
+
+        for index_out, outp in enumerate(component_copy.outputs):
+            for signal in outp.signals:
+                signal.remove_source()
+                signal.set_source(component.output(index_out))
+
+        return sfg_copy()  # Copy again to update IDs.
+
+    def insert_operation(self, component: Operation, output_comp_id: GraphID) -> Optional["SFG"]:
+        """Insert an operation in the SFG after a given source operation.
+        The source operation output count must match the input count of the operation as well as the output
+        Then return a new deepcopy of the sfg with the inserted component.
+
+        Arguments:
+        component: The new component, e.g Multiplication.
+        output_comp_id: The source operation GraphID to connect from.
+        """
+
+        # Preserve the original SFG by creating a copy.
+        sfg_copy = self()
+        output_comp = sfg_copy.find_by_id(output_comp_id)
+        if output_comp is None:
+            return None
+
+        assert not isinstance(output_comp, Output), \
+            "Source operation can not be an output operation."
+        assert len(output_comp.output_signals) == component.input_count, \
+            "Source operation output count does not match input count for component."
+        assert len(output_comp.output_signals) == component.output_count, \
+            "Destination operation input count does not match output for component."
+
+        for index, signal_in in enumerate(output_comp.output_signals):
+            destination = signal_in.destination
+            signal_in.set_destination(component.input(index))
+            destination.connect(component.output(index))
+
+        # Recreate the newly coupled SFG so that all attributes are correct.
+        return sfg_copy()
+
+    def remove_operation(self, operation_id: GraphID) -> "SFG":
+        """Returns a version of the SFG where the operation with the specified GraphID removed.
+        The operation has to have the same amount of input- and output ports or a ValueError will 
+        be raised. If no operation with the entered operation_id is found then returns None and does nothing."""
+        sfg_copy = self()
+        operation = sfg_copy.find_by_id(operation_id)
+        if operation is None:
+            return None
+
+        if operation.input_count != operation.output_count:
+            raise ValueError(
+                "Different number of input and output ports of operation with the specified id")
+
+        for i, outport in enumerate(operation.outputs):
+            if outport.signal_count > 0:
+                if operation.input(i).signal_count > 0 and operation.input(i).signals[0].source is not None:
+                    in_sig = operation.input(i).signals[0]
+                    source_port = in_sig.source
+                    source_port.remove_signal(in_sig)
+                    operation.input(i).remove_signal(in_sig)
+                    for out_sig in outport.signals.copy():
+                        out_sig.set_source(source_port)
+                else:
+                    for out_sig in outport.signals.copy():
+                        out_sig.remove_source()
+            else:
+                if operation.input(i).signal_count > 0:
+                    in_sig = operation.input(i).signals[0]
+                    operation.input(i).remove_signal(in_sig)
+
+        return sfg_copy()
+
+    def get_precedence_list(self) -> Sequence[Sequence[OutputPort]]:
+        """Returns a Precedence list of the SFG where each element in n:th the list consists
+        of elements that are executed in the n:th step. If the precedence list already has been
+        calculated for the current SFG then returns the cached version."""
+        if self._precedence_list:
+            return self._precedence_list
+
+        # Find all operations with only outputs and no inputs.
+        no_input_ops = list(
+            filter(lambda op: op.input_count == 0, self.operations))
+        delay_ops = self.find_by_type_name(Delay.type_name())
+
+        # Find all first iter output ports for precedence
+        first_iter_ports = [op.output(i) for op in (
+            no_input_ops + delay_ops) for i in range(op.output_count)]
+
+        self._precedence_list = self._traverse_for_precedence_list(
+            first_iter_ports)
+
+        return self._precedence_list
+
+    def show_precedence_graph(self) -> None:
+        p_list = self.get_precedence_list()
+        pg = Digraph()
+        pg.attr(rankdir='LR')
+
+        # Creates nodes for each output port in the precedence list
+        for i in range(len(p_list)):
+            ports = p_list[i]
+            with pg.subgraph(name='cluster_' + str(i)) as sub:
+                sub.attr(label='N' + str(i + 1))
+                for port in ports:
+                    sub.node(port.operation.graph_id + '.' + str(port.index))
+        # Creates edges for each output port and creates nodes for each operation and edges for them as well
+        for i in range(len(p_list)):
+            ports = p_list[i]
+            for port in ports:
+                for signal in port.signals:
+                    pg.edge(port.operation.graph_id + '.' + str(port.index),
+                            signal.destination.operation.graph_id)
+                    pg.node(signal.destination.operation.graph_id,
+                            shape='square')
+                pg.edge(port.operation.graph_id,
+                        port.operation.graph_id + '.' + str(port.index))
+                pg.node(port.operation.graph_id, shape='square')
+
+        pg.view()
+
+    def print_precedence_graph(self) -> None:
+        """Prints a representation of the SFG's precedence list to the standard out.
+        If the precedence list already has been calculated then it uses the cached version,
+        otherwise it calculates the precedence list and then prints it."""
+        precedence_list = self.get_precedence_list()
+
+        line = "-" * 120
+        out_str = StringIO()
+        out_str.write(line)
+
+        printed_ops = set()
+
+        for iter_num, iter in enumerate(precedence_list, start=1):
+            for outport_num, outport in enumerate(iter, start=1):
+                if outport not in printed_ops:
+                    # Only print once per operation, even if it has multiple outports
+                    out_str.write("\n")
+                    out_str.write(str(iter_num))
+                    out_str.write(".")
+                    out_str.write(str(outport_num))
+                    out_str.write(" \t")
+                    out_str.write(str(outport.operation))
+                    printed_ops.add(outport)
+
+            out_str.write("\n")
+            out_str.write(line)
+
+        print(out_str.getvalue())
+
+    def get_operations_topological_order(self) -> Iterable[Operation]:
+        """Returns an Iterable of the Operations in the SFG in Topological Order.
+        Feedback loops makes an absolutely correct Topological order impossible, so an 
+        approximative Topological Order is returned in such cases in this implementation."""
+        if self._operations_topological_order:
+            return self._operations_topological_order
+
+        no_inputs_queue = deque(
+            list(filter(lambda op: op.input_count == 0, self.operations)))
+        remaining_inports_per_operation = {
+            op: op.input_count for op in self.operations}
+
+        # Maps number of input counts to a queue of seen objects with such a size.
+        seen_with_inputs_dict = defaultdict(deque)
+        seen = set()
+        top_order = []
+
+        assert len(
+            no_inputs_queue) > 0, "Illegal SFG state, dangling signals in SFG."
+
+        first_op = no_inputs_queue.popleft()
+        visited = set([first_op])
+        p_queue = PriorityQueue()
+        p_queue_entry_num = it.count()
+        # Negative priority as max-heap popping is wanted
+        p_queue.put((-first_op.output_count, -
+                     next(p_queue_entry_num), first_op))
+
+        operations_left = len(self.operations) - 1
+
+        seen_but_not_visited_count = 0
+
+        while operations_left > 0:
+            while not p_queue.empty():
+
+                op = p_queue.get()[2]
+
+                operations_left -= 1
+                top_order.append(op)
+                visited.add(op)
+
+                for neighbor_op in op.subsequent_operations:
+                    if neighbor_op not in visited:
+                        remaining_inports_per_operation[neighbor_op] -= 1
+                        remaining_inports = remaining_inports_per_operation[neighbor_op]
+
+                        if remaining_inports == 0:
+                            p_queue.put(
+                                (-neighbor_op.output_count, -next(p_queue_entry_num), neighbor_op))
+
+                        elif remaining_inports > 0:
+                            if neighbor_op in seen:
+                                seen_with_inputs_dict[remaining_inports +
+                                                      1].remove(neighbor_op)
+                            else:
+                                seen.add(neighbor_op)
+                                seen_but_not_visited_count += 1
+
+                            seen_with_inputs_dict[remaining_inports].append(
+                                neighbor_op)
+
+            # Check if have to fetch Operations from somewhere else since p_queue is empty
+            if operations_left > 0:
+                # First check if can fetch from Operations with no input ports
+                if no_inputs_queue:
+                    new_op = no_inputs_queue.popleft()
+                    p_queue.put((-new_op.output_count, -
+                                 next(p_queue_entry_num), new_op))
+
+                # Else fetch operation with lowest input count that is not zero
+                elif seen_but_not_visited_count > 0:
+                    for i in it.count(start=1):
+                        seen_inputs_queue = seen_with_inputs_dict[i]
+                        if seen_inputs_queue:
+                            new_op = seen_inputs_queue.popleft()
+                            p_queue.put((-new_op.output_count, -
+                                         next(p_queue_entry_num), new_op))
+                            seen_but_not_visited_count -= 1
+                            break
+                else:
+                    raise RuntimeError("Unallowed structure in SFG detected")
+
+        self._operations_topological_order = top_order
+        return self._operations_topological_order
+
+    def set_latency_of_type(self, type_name: TypeName, latency: int) -> None:
+        """Set the latency of all components with the given type name."""
+        for op in self.find_by_type_name(type_name):
+            op.set_latency(latency)
+
+    def set_latency_offsets_of_type(self, type_name: TypeName, latency_offsets: Dict[str, int]) -> None:
+        """Set the latency offset of all components with the given type name."""
+        for op in self.find_by_type_name(type_name):
+            op.set_latency_offsets(latency_offsets)
+
+    def _traverse_for_precedence_list(self, first_iter_ports: List[OutputPort]) -> List[List[OutputPort]]:
+        # Find dependencies of output ports and input ports.
+        remaining_inports_per_operation = {
+            op: op.input_count for op in self.operations}
+
+        # Traverse output ports for precedence
+        curr_iter_ports = first_iter_ports
+        precedence_list = []
+
+        while curr_iter_ports:
+            # Add the found ports to the current iter
+            precedence_list.append(curr_iter_ports)
+
+            next_iter_ports = []
+
+            for outport in curr_iter_ports:
+                for signal in outport.signals:
+                    new_inport = signal.destination
+                    # Don't traverse over delays.
+                    if new_inport is not None and not isinstance(new_inport.operation, Delay):
+                        new_op = new_inport.operation
+                        remaining_inports_per_operation[new_op] -= 1
+                        if remaining_inports_per_operation[new_op] == 0:
+                            next_iter_ports.extend(new_op.outputs)
+
+            curr_iter_ports = next_iter_ports
+
+        return precedence_list
+
+    def _add_component_unconnected_copy(self, original_component: GraphComponent) -> GraphComponent:
+        assert original_component not in self._original_components_to_new, "Tried to add duplicate SFG component"
+        new_component = original_component.copy_component()
+        self._original_components_to_new[original_component] = new_component
+        new_id = self._graph_id_generator.next_id(new_component.type_name())
+        new_component.graph_id = new_id
+        self._components_by_id[new_id] = new_component
+        self._components_by_name[new_component.name].append(new_component)
+        return new_component
+
+    def _add_operation_connected_tree_copy(self, start_op: Operation) -> None:
+        op_stack = deque([start_op])
+        while op_stack:
+            original_op = op_stack.pop()
+            # Add or get the new copy of the operation.
+            new_op = None
+            if original_op not in self._original_components_to_new:
+                new_op = self._add_component_unconnected_copy(original_op)
+                self._components_dfs_order.append(new_op)
+                self._operations_dfs_order.append(new_op)
+            else:
+                new_op = self._original_components_to_new[original_op]
+
+            # Connect input ports to new signals.
+            for original_input_port in original_op.inputs:
+                if original_input_port.signal_count < 1:
+                    raise ValueError("Unconnected input port in SFG")
+
+                for original_signal in original_input_port.signals:
+                    # Check if the signal is one of the SFG's input signals.
+                    if original_signal in self._original_input_signals_to_indices:
+                        # New signal already created during first step of constructor.
+                        new_signal = self._original_components_to_new[original_signal]
+                        new_signal.set_destination(
+                            new_op.input(original_input_port.index))
+
+                        self._components_dfs_order.extend(
+                            [new_signal, new_signal.source.operation])
+                        self._operations_dfs_order.append(
+                            new_signal.source.operation)
+
+                    # Check if the signal has not been added before.
+                    elif original_signal not in self._original_components_to_new:
+                        if original_signal.source is None:
+                            raise ValueError(
+                                "Dangling signal without source in SFG")
+
+                        new_signal = self._add_component_unconnected_copy(
+                            original_signal)
+                        new_signal.set_destination(
+                            new_op.input(original_input_port.index))
+
+                        self._components_dfs_order.append(new_signal)
+
+                        original_connected_op = original_signal.source.operation
+                        # Check if connected Operation has been added before.
+                        if original_connected_op in self._original_components_to_new:
+                            # Set source to the already added operations port.
+                            new_signal.set_source(self._original_components_to_new[original_connected_op].output(
+                                original_signal.source.index))
+                        else:
+                            # Create new operation, set signal source to it.
+                            new_connected_op = self._add_component_unconnected_copy(
+                                original_connected_op)
+                            new_signal.set_source(new_connected_op.output(
+                                original_signal.source.index))
+
+                            self._components_dfs_order.append(new_connected_op)
+                            self._operations_dfs_order.append(new_connected_op)
+
+                            # Add connected operation to queue of operations to visit.
+                            op_stack.append(original_connected_op)
+
+            # Connect output ports.
+            for original_output_port in original_op.outputs:
+                for original_signal in original_output_port.signals:
+                    # Check if the signal is one of the SFG's output signals.
+                    if original_signal in self._original_output_signals_to_indices:
+                        # New signal already created during first step of constructor.
+                        new_signal = self._original_components_to_new[original_signal]
+                        new_signal.set_source(
+                            new_op.output(original_output_port.index))
+
+                        self._components_dfs_order.extend(
+                            [new_signal, new_signal.destination.operation])
+                        self._operations_dfs_order.append(
+                            new_signal.destination.operation)
+
+                    # Check if signal has not been added before.
+                    elif original_signal not in self._original_components_to_new:
+                        if original_signal.source is None:
+                            raise ValueError(
+                                "Dangling signal without source in SFG")
+
+                        new_signal = self._add_component_unconnected_copy(
+                            original_signal)
+                        new_signal.set_source(
+                            new_op.output(original_output_port.index))
+
+                        self._components_dfs_order.append(new_signal)
+
+                        original_connected_op = original_signal.destination.operation
+                        # Check if connected operation has been added.
+                        if original_connected_op in self._original_components_to_new:
+                            # Set destination to the already connected operations port.
+                            new_signal.set_destination(self._original_components_to_new[original_connected_op].input(
+                                original_signal.destination.index))
+                        else:
+                            # Create new operation, set destination to it.
+                            new_connected_op = self._add_component_unconnected_copy(
+                                original_connected_op)
+                            new_signal.set_destination(new_connected_op.input(
+                                original_signal.destination.index))
+
+                            self._components_dfs_order.append(new_connected_op)
+                            self._operations_dfs_order.append(new_connected_op)
+
+                            # Add connected operation to the queue of operations to visit.
+                            op_stack.append(original_connected_op)
+
+    def _evaluate_source(self, src: OutputPort, results: MutableResultMap, delays: MutableDelayMap, prefix: str, bits_override: Optional[int], truncate: bool, deferred_delays: DelayQueue) -> Number:
+        key_base = (
+            prefix + "." + src.operation.graph_id) if prefix else src.operation.graph_id
+        key = src.operation.key(src.index, key_base)
+        if key in results:
+            value = results[key]
+            if value is None:
+                raise RuntimeError(
+                    "Direct feedback loop detected when evaluating operation.")
+            return value
+
+        value = src.operation.current_output(src.index, delays, key_base)
+        results[key] = value
+        if value is None:
+            value = self._do_evaluate_source(
+                key_base, key, src, results, delays, prefix, bits_override, truncate, deferred_delays)
+        else:
+            # Evaluate later. Use current value for now.
+            deferred_delays.append((key_base, key, src))
+        return value
+
+    def _do_evaluate_source(self, key_base: str, key: ResultKey, src: OutputPort, results: MutableResultMap, delays: MutableDelayMap, prefix: str, bits_override: Optional[int], truncate: bool, deferred_delays: DelayQueue) -> Number:
+        input_values = [self._evaluate_source(input_port.signals[0].source, results, delays, prefix,
+                                              bits_override, truncate, deferred_delays) for input_port in src.operation.inputs]
+        value = src.operation.evaluate_output(
+            src.index, input_values, results, delays, key_base, bits_override, truncate)
+        results[key] = value
+        return value
diff --git a/b_asic/simulation.py b/b_asic/simulation.py
index 50adaa522b6d685b428354a9f84689330b7fd40f..5fece8d78f08b5273313bedf7e0a7bdbeebe483b 100644
--- a/b_asic/simulation.py
+++ b/b_asic/simulation.py
@@ -1,35 +1,125 @@
-"""@package docstring
-B-ASIC Simulation Module.
-TODO: More info.
+"""B-ASIC Simulation Module.
+
+Contains a class for simulating the result of an SFG given a set of input values.
 """
 
+import numpy as np
+
+from collections import defaultdict
 from numbers import Number
-from typing import List
+from typing import List, Dict, DefaultDict, Callable, Sequence, Mapping, Union, Optional, MutableSequence, MutableMapping
 
+from b_asic.operation import ResultKey, ResultMap, MutableResultMap, MutableDelayMap
+from b_asic.signal_flow_graph import SFG
 
-class OperationState:
-    """Simulation state of an operation.
-    TODO: More info.
-    """
 
-    output_values: List[Number]
-    iteration: int
+ResultArrayMap = Mapping[ResultKey, Sequence[Number]]
+MutableResultArrayMap = MutableMapping[ResultKey, MutableSequence[Number]]
+InputFunction = Callable[[int], Number]
+InputProvider = Union[Number, Sequence[Number], InputFunction]
 
-    def __init__(self):
-        self.output_values = []
-        self.iteration = 0
 
+class Simulation:
+    """Simulation of an SFG.
 
-class SimulationState:
-    """Simulation state.
-    TODO: More info.
+    Use FastSimulation (from the C++ extension module) for a more effective
+    simulation when running many iterations.
     """
 
-    # operation_states: Dict[OperationId, OperationState]
-    iteration: int
+    _sfg: SFG
+    _results: MutableResultArrayMap
+    _delays: MutableDelayMap
+    _iteration: int
+    _input_functions: Sequence[InputFunction]
+    _input_length: Optional[int]
+
+    def __init__(self, sfg: SFG, input_providers: Optional[Sequence[Optional[InputProvider]]] = None):
+        """Construct a Simulation of an SFG."""
+        self._sfg = sfg()  # Copy the SFG to make sure it's not modified from the outside.
+        self._results = defaultdict(list)
+        self._delays = {}
+        self._iteration = 0
+        self._input_functions = [
+            lambda _: 0 for _ in range(self._sfg.input_count)]
+        self._input_length = None
+        if input_providers is not None:
+            self.set_inputs(input_providers)
+
+    def set_input(self, index: int, input_provider: InputProvider) -> None:
+        """Set the input function used to get values for the specific input at the given index to the internal SFG."""
+        if index < 0 or index >= len(self._input_functions):
+            raise IndexError(
+                f"Input index out of range (expected 0-{len(self._input_functions) - 1}, got {index})")
+        if callable(input_provider):
+            self._input_functions[index] = input_provider
+        elif isinstance(input_provider, Number):
+            self._input_functions[index] = lambda _: input_provider
+        else:
+            if self._input_length is None:
+                self._input_length = len(input_provider)
+            elif self._input_length != len(input_provider):
+                raise ValueError(
+                    f"Inconsistent input length for simulation (was {self._input_length}, got {len(input_provider)})")
+            self._input_functions[index] = lambda n: input_provider[n]
+
+    def set_inputs(self, input_providers: Sequence[Optional[InputProvider]]) -> None:
+        """Set the input functions used to get values for the inputs to the internal SFG."""
+        if len(input_providers) != self._sfg.input_count:
+            raise ValueError(
+                f"Wrong number of inputs supplied to simulation (expected {self._sfg.input_count}, got {len(input_providers)})")
+        for index, input_provider in enumerate(input_providers):
+            if input_provider is not None:
+                self.set_input(index, input_provider)
+
+    def step(self, save_results: bool = True, bits_override: Optional[int] = None, truncate: bool = True) -> Sequence[Number]:
+        """Run one iteration of the simulation and return the resulting output values."""
+        return self.run_for(1, save_results, bits_override, truncate)
+
+    def run_until(self, iteration: int, save_results: bool = True, bits_override: Optional[int] = None, truncate: bool = True) -> Sequence[Number]:
+        """Run the simulation until its iteration is greater than or equal to the given iteration
+        and return the output values of the last iteration.
+        """
+        result = []
+        while self._iteration < iteration:
+            input_values = [self._input_functions[i](
+                self._iteration) for i in range(self._sfg.input_count)]
+            results = {}
+            result = self._sfg.evaluate_outputs(
+                input_values, results, self._delays, "", bits_override, truncate)
+            if save_results:
+                for key, value in results.items():
+                    self._results[key].append(value)
+            self._iteration += 1
+        return result
+
+    def run_for(self, iterations: int, save_results: bool = True, bits_override: Optional[int] = None, truncate: bool = True) -> Sequence[Number]:
+        """Run a given number of iterations of the simulation and return the output values of the last iteration."""
+        return self.run_until(self._iteration + iterations, save_results, bits_override, truncate)
+
+    def run(self, save_results: bool = True, bits_override: Optional[int] = None, truncate: bool = True) -> Sequence[Number]:
+        """Run the simulation until the end of its input arrays and return the output values of the last iteration."""
+        if self._input_length is None:
+            raise IndexError("Tried to run unlimited simulation")
+        return self.run_until(self._input_length, save_results, bits_override, truncate)
+
+    @property
+    def iteration(self) -> int:
+        """Get the current iteration number of the simulation."""
+        return self._iteration
+
+    @property
+    def results(self) -> ResultArrayMap:
+        """Get a mapping from result keys to numpy arrays containing all results, including intermediate values,
+        calculated for each iteration up until now that was run with save_results enabled.
+        The mapping is indexed using the key() method of Operation with the appropriate output index.
+        Example result after 3 iterations: {"c1": [3, 6, 7], "c2": [4, 5, 5], "bfly1.0": [7, 0, 0], "bfly1.1": [-1, 0, 2], "0": [7, -2, -1]}
+        """
+        return {key: np.array(value) for key, value in self._results.items()}
 
-    def __init__(self):
-        self.operation_states = {}
-        self.iteration = 0
+    def clear_results(self) -> None:
+        """Clear all results that were saved until now."""
+        self._results.clear()
 
-    # TODO: More stuff.
+    def clear_state(self) -> None:
+        """Clear all current state of the simulation, except for the results and iteration."""
+        self._delays.clear()
diff --git a/b_asic/special_operations.py b/b_asic/special_operations.py
new file mode 100644
index 0000000000000000000000000000000000000000..dc84f0bcb8186b0db31ca96551f2876f48b9b523
--- /dev/null
+++ b/b_asic/special_operations.py
@@ -0,0 +1,117 @@
+"""B-ASIC Special Operations Module.
+
+Contains operations with special purposes that may be treated differently from
+normal operations in an SFG.
+"""
+
+from numbers import Number
+from typing import Optional, Sequence
+
+from b_asic.operation import AbstractOperation, ResultKey, DelayMap, MutableResultMap, MutableDelayMap
+from b_asic.graph_component import Name, TypeName
+from b_asic.port import SignalSourceProvider
+
+
+class Input(AbstractOperation):
+    """Input operation.
+
+    Marks an input port to an SFG.
+    Its value will be updated on each iteration when simulating the SFG.
+    """
+
+    def __init__(self, name: Name = ""):
+        """Construct an Input operation."""
+        super().__init__(input_count=0, output_count=1, name=name)
+        self.set_param("value", 0)
+
+    @classmethod
+    def type_name(cls) -> TypeName:
+        return "in"
+
+    def evaluate(self):
+        return self.param("value")
+
+    @property
+    def value(self) -> Number:
+        """Get the current value of this input."""
+        return self.param("value")
+
+    @value.setter
+    def value(self, value: Number) -> None:
+        """Set the current value of this input."""
+        self.set_param("value", value)
+
+
+class Output(AbstractOperation):
+    """Output operation.
+
+    Marks an output port to an SFG.
+    The SFG will forward its input to the corresponding output signal
+    destinations.
+    """
+
+    def __init__(self, src0: Optional[SignalSourceProvider] = None, name: Name = ""):
+        """Construct an Output operation."""
+        super().__init__(input_count=1, output_count=0,
+                         name=name, input_sources=[src0])
+
+    @classmethod
+    def type_name(cls) -> TypeName:
+        return "out"
+
+    def evaluate(self, _):
+        return None
+
+
+class Delay(AbstractOperation):
+    """Unit delay operation.
+
+    Represents one unit of delay in a circuit, typically a clock cycle.
+    Can be thought of as a register or a D flip-flop.
+    """
+
+    def __init__(self, src0: Optional[SignalSourceProvider] = None, initial_value: Number = 0, name: Name = ""):
+        """Construct a Delay operation."""
+        super().__init__(input_count=1, output_count=1,
+                         name=name, input_sources=[src0])
+        self.set_param("initial_value", initial_value)
+
+    @classmethod
+    def type_name(cls) -> TypeName:
+        return "t"
+
+    def evaluate(self, a):
+        return self.param("initial_value")
+
+    def current_output(self, index: int, delays: Optional[DelayMap] = None, prefix: str = "") -> Optional[Number]:
+        if delays is not None:
+            return delays.get(self.key(index, prefix), self.param("initial_value"))
+        return self.param("initial_value")
+
+    def evaluate_output(self, index: int, input_values: Sequence[Number], results: Optional[MutableResultMap] = None, delays: Optional[MutableDelayMap] = None, prefix: str = "", bits_override: Optional[int] = None, truncate: bool = True) -> Number:
+        if index != 0:
+            raise IndexError(
+                f"Output index out of range (expected 0-0, got {index})")
+        if len(input_values) != 1:
+            raise ValueError(
+                f"Wrong number of inputs supplied to SFG for evaluation (expected 1, got {len(input_values)})")
+
+        key = self.key(index, prefix)
+        value = self.param("initial_value")
+        if delays is not None:
+            value = delays.get(key, value)
+            delays[key] = self.truncate_inputs(input_values, bits_override)[
+                0] if truncate else input_values[0]
+        if results is not None:
+            results[key] = value
+        return value
+
+    @property
+    def initial_value(self) -> Number:
+        """Get the initial value of this delay."""
+        return self.param("initial_value")
+
+    @initial_value.setter
+    def initial_value(self, value: Number) -> None:
+        """Set the initial value of this delay."""
+        self.set_param("initial_value", value)
diff --git a/legacy/README.md b/legacy/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..746d1efdb57c04f4e52bb2ce7f0a7def99c744ed
--- /dev/null
+++ b/legacy/README.md
@@ -0,0 +1,11 @@
+# Legacy files
+
+This folder contains currently unused code that is kept for acedemic purposes,
+or to be used as a refererence for future development.
+
+## simulation_oop
+
+This folder contains a C++ implementation of the Simulation class designed
+using Object-Oriented Programming, as opposed to the current version that uses
+Data-Oriented Design. They are functionally identical, but use different
+styles of programming and have different performance characteristics.
\ No newline at end of file
diff --git a/legacy/simulation_oop/core_operations.h b/legacy/simulation_oop/core_operations.h
new file mode 100644
index 0000000000000000000000000000000000000000..0926572905053cdf5c762f73545642f1ffe3d4f8
--- /dev/null
+++ b/legacy/simulation_oop/core_operations.h
@@ -0,0 +1,236 @@
+#ifndef ASIC_SIMULATION_CORE_OPERATIONS_H
+#define ASIC_SIMULATION_CORE_OPERATIONS_H
+
+#define NOMINMAX
+#include "../debug.h"
+#include "../number.h"
+#include "operation.h"
+
+#include <algorithm>
+#include <cmath>
+#include <cstddef>
+#include <stdexcept>
+#include <utility>
+
+namespace asic {
+
+class constant_operation final : public abstract_operation {
+public:
+	constant_operation(result_key key, number value)
+		: abstract_operation(std::move(key))
+		, m_value(value) {}
+
+	[[nodiscard]] std::size_t output_count() const noexcept final {
+		return 1;
+	}
+
+private:
+	[[nodiscard]] number evaluate_output_impl(std::size_t, evaluation_context const&) const final {
+		ASIC_DEBUG_MSG("Evaluating constant.");
+		return m_value;
+	}
+
+	number m_value;
+};
+
+class addition_operation final : public binary_operation {
+public:
+	explicit addition_operation(result_key key)
+		: binary_operation(std::move(key)) {}
+
+	[[nodiscard]] std::size_t output_count() const noexcept final {
+		return 1;
+	}
+
+private:
+	[[nodiscard]] number evaluate_output_impl(std::size_t, evaluation_context const& context) const final {
+		ASIC_DEBUG_MSG("Evaluating addition.");
+		return this->evaluate_lhs(context) + this->evaluate_rhs(context);
+	}
+};
+
+class subtraction_operation final : public binary_operation {
+public:
+	explicit subtraction_operation(result_key key)
+		: binary_operation(std::move(key)) {}
+
+	[[nodiscard]] std::size_t output_count() const noexcept final {
+		return 1;
+	}
+
+private:
+	[[nodiscard]] number evaluate_output_impl(std::size_t, evaluation_context const& context) const final {
+		ASIC_DEBUG_MSG("Evaluating subtraction.");
+		return this->evaluate_lhs(context) - this->evaluate_rhs(context);
+	}
+};
+
+class multiplication_operation final : public binary_operation {
+public:
+	explicit multiplication_operation(result_key key)
+		: binary_operation(std::move(key)) {}
+
+	[[nodiscard]] std::size_t output_count() const noexcept final {
+		return 1;
+	}
+
+private:
+	[[nodiscard]] number evaluate_output_impl(std::size_t, evaluation_context const& context) const final {
+		ASIC_DEBUG_MSG("Evaluating multiplication.");
+		return this->evaluate_lhs(context) * this->evaluate_rhs(context);
+	}
+};
+
+class division_operation final : public binary_operation {
+public:
+	explicit division_operation(result_key key)
+		: binary_operation(std::move(key)) {}
+
+	[[nodiscard]] std::size_t output_count() const noexcept final {
+		return 1;
+	}
+
+private:
+	[[nodiscard]] number evaluate_output_impl(std::size_t, evaluation_context const& context) const final {
+		ASIC_DEBUG_MSG("Evaluating division.");
+		return this->evaluate_lhs(context) / this->evaluate_rhs(context);
+	}
+};
+
+class min_operation final : public binary_operation {
+public:
+	explicit min_operation(result_key key)
+		: binary_operation(std::move(key)) {}
+
+	[[nodiscard]] std::size_t output_count() const noexcept final {
+		return 1;
+	}
+
+private:
+	[[nodiscard]] number evaluate_output_impl(std::size_t, evaluation_context const& context) const final {
+		ASIC_DEBUG_MSG("Evaluating min.");
+		auto const lhs = this->evaluate_lhs(context);
+		if (lhs.imag() != 0) {
+			throw std::runtime_error{"Min does not support complex numbers."};
+		}
+		auto const rhs = this->evaluate_rhs(context);
+		if (rhs.imag() != 0) {
+			throw std::runtime_error{"Min does not support complex numbers."};
+		}
+		return std::min(lhs.real(), rhs.real());
+	}
+};
+
+class max_operation final : public binary_operation {
+public:
+	explicit max_operation(result_key key)
+		: binary_operation(std::move(key)) {}
+
+	[[nodiscard]] std::size_t output_count() const noexcept final {
+		return 1;
+	}
+
+private:
+	[[nodiscard]] number evaluate_output_impl(std::size_t, evaluation_context const& context) const final {
+		ASIC_DEBUG_MSG("Evaluating max.");
+		auto const lhs = this->evaluate_lhs(context);
+		if (lhs.imag() != 0) {
+			throw std::runtime_error{"Max does not support complex numbers."};
+		}
+		auto const rhs = this->evaluate_rhs(context);
+		if (rhs.imag() != 0) {
+			throw std::runtime_error{"Max does not support complex numbers."};
+		}
+		return std::max(lhs.real(), rhs.real());
+	}
+};
+
+class square_root_operation final : public unary_operation {
+public:
+	explicit square_root_operation(result_key key)
+		: unary_operation(std::move(key)) {}
+
+	[[nodiscard]] std::size_t output_count() const noexcept final {
+		return 1;
+	}
+
+private:
+	[[nodiscard]] number evaluate_output_impl(std::size_t, evaluation_context const& context) const final {
+		ASIC_DEBUG_MSG("Evaluating sqrt.");
+		return std::sqrt(this->evaluate_input(context));
+	}
+};
+
+class complex_conjugate_operation final : public unary_operation {
+public:
+	explicit complex_conjugate_operation(result_key key)
+		: unary_operation(std::move(key)) {}
+
+	[[nodiscard]] std::size_t output_count() const noexcept final {
+		return 1;
+	}
+
+private:
+	[[nodiscard]] number evaluate_output_impl(std::size_t, evaluation_context const& context) const final {
+		ASIC_DEBUG_MSG("Evaluating conj.");
+		return std::conj(this->evaluate_input(context));
+	}
+};
+
+class absolute_operation final : public unary_operation {
+public:
+	explicit absolute_operation(result_key key)
+		: unary_operation(std::move(key)) {}
+
+	[[nodiscard]] std::size_t output_count() const noexcept final {
+		return 1;
+	}
+
+private:
+	[[nodiscard]] number evaluate_output_impl(std::size_t, evaluation_context const& context) const final {
+		ASIC_DEBUG_MSG("Evaluating abs.");
+		return std::abs(this->evaluate_input(context));
+	}
+};
+
+class constant_multiplication_operation final : public unary_operation {
+public:
+	constant_multiplication_operation(result_key key, number value)
+		: unary_operation(std::move(key))
+		, m_value(value) {}
+
+	[[nodiscard]] std::size_t output_count() const noexcept final {
+		return 1;
+	}
+
+private:
+	[[nodiscard]] number evaluate_output_impl(std::size_t, evaluation_context const& context) const final {
+		ASIC_DEBUG_MSG("Evaluating cmul.");
+		return this->evaluate_input(context) * m_value;
+	}
+
+	number m_value;
+};
+
+class butterfly_operation final : public binary_operation {
+public:
+	explicit butterfly_operation(result_key key)
+		: binary_operation(std::move(key)) {}
+
+	[[nodiscard]] std::size_t output_count() const noexcept final {
+		return 2;
+	}
+
+private:
+	[[nodiscard]] number evaluate_output_impl(std::size_t index, evaluation_context const& context) const final {
+		ASIC_DEBUG_MSG("Evaluating bfly.");
+		if (index == 0) {
+			return this->evaluate_lhs(context) + this->evaluate_rhs(context);
+		}
+		return this->evaluate_lhs(context) - this->evaluate_rhs(context);
+	}
+};
+
+} // namespace asic
+
+#endif // ASIC_SIMULATION_CORE_OPERATIONS_H
\ No newline at end of file
diff --git a/legacy/simulation_oop/custom_operation.cpp b/legacy/simulation_oop/custom_operation.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9153eb5b651f3a481747f3b652f790ce581e70dc
--- /dev/null
+++ b/legacy/simulation_oop/custom_operation.cpp
@@ -0,0 +1,30 @@
+#include "custom_operation.h"
+
+#include <pybind11/stl.h>
+
+namespace py = pybind11;
+
+namespace asic {
+
+custom_operation::custom_operation(result_key key, pybind11::object evaluate_output, pybind11::object truncate_input,
+								   std::size_t output_count)
+	: nary_operation(std::move(key))
+	, m_evaluate_output(std::move(evaluate_output))
+	, m_truncate_input(std::move(truncate_input))
+	, m_output_count(output_count) {}
+
+std::size_t custom_operation::output_count() const noexcept {
+	return m_output_count;
+}
+
+number custom_operation::evaluate_output_impl(std::size_t index, evaluation_context const& context) const {
+	using namespace pybind11::literals;
+	auto input_values = this->evaluate_inputs(context);
+	return m_evaluate_output(index, std::move(input_values), "truncate"_a = false).cast<number>();
+}
+
+number custom_operation::truncate_input(std::size_t index, number value, std::size_t bits) const {
+	return m_truncate_input(index, value, bits).cast<number>();
+}
+
+} // namespace asic
\ No newline at end of file
diff --git a/legacy/simulation_oop/custom_operation.h b/legacy/simulation_oop/custom_operation.h
new file mode 100644
index 0000000000000000000000000000000000000000..8a11aaacbc8c17500069522d9d8f56d9c416d804
--- /dev/null
+++ b/legacy/simulation_oop/custom_operation.h
@@ -0,0 +1,35 @@
+#ifndef ASIC_SIMULATION_CUSTOM_OPERATION_H
+#define ASIC_SIMULATION_CUSTOM_OPERATION_H
+
+#include "../algorithm.h"
+#include "../debug.h"
+#include "../number.h"
+#include "operation.h"
+
+#include <cstddef>
+#include <fmt/format.h>
+#include <functional>
+#include <pybind11/pybind11.h>
+#include <stdexcept>
+#include <utility>
+
+namespace asic {
+
+class custom_operation final : public nary_operation {
+public:
+	custom_operation(result_key key, pybind11::object evaluate_output, pybind11::object truncate_input, std::size_t output_count);
+
+	[[nodiscard]] std::size_t output_count() const noexcept final;
+
+private:
+	[[nodiscard]] number evaluate_output_impl(std::size_t index, evaluation_context const& context) const final;
+	[[nodiscard]] number truncate_input(std::size_t index, number value, std::size_t bits) const final;
+
+	pybind11::object m_evaluate_output;
+	pybind11::object m_truncate_input;
+	std::size_t m_output_count;
+};
+
+} // namespace asic
+
+#endif // ASIC_SIMULATION_CUSTOM_OPERATION_H
\ No newline at end of file
diff --git a/legacy/simulation_oop/operation.cpp b/legacy/simulation_oop/operation.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a9738a0a05287f6ab2d430d4c73560a4c6bd57c5
--- /dev/null
+++ b/legacy/simulation_oop/operation.cpp
@@ -0,0 +1,156 @@
+#include "operation.h"
+
+#include "../debug.h"
+
+#include <pybind11/pybind11.h>
+
+namespace py = pybind11;
+
+namespace asic {
+
+signal_source::signal_source(std::shared_ptr<const operation> op, std::size_t index, std::optional<std::size_t> bits)
+	: m_operation(std::move(op))
+	, m_index(index)
+	, m_bits(std::move(bits)) {}
+
+signal_source::operator bool() const noexcept {
+	return static_cast<bool>(m_operation);
+}
+
+std::optional<number> signal_source::current_output(delay_map const& delays) const {
+	ASIC_ASSERT(m_operation);
+	return m_operation->current_output(m_index, delays);
+}
+
+number signal_source::evaluate_output(evaluation_context const& context) const {
+	ASIC_ASSERT(m_operation);
+	return m_operation->evaluate_output(m_index, context);
+}
+
+std::optional<std::size_t> signal_source::bits() const noexcept {
+	return m_bits;
+}
+
+abstract_operation::abstract_operation(result_key key)
+	: m_key(std::move(key)) {}
+
+std::optional<number> abstract_operation::current_output(std::size_t, delay_map const&) const {
+	return std::nullopt;
+}
+
+number abstract_operation::evaluate_output(std::size_t index, evaluation_context const& context) const {
+	ASIC_ASSERT(index < this->output_count());
+	ASIC_ASSERT(context.results);
+	auto const key = this->key_of_output(index);
+	if (auto const it = context.results->find(key); it != context.results->end()) {
+		if (it->second) {
+			return *it->second;
+		}
+		throw std::runtime_error{"Direct feedback loop detected when evaluating simulation operation."};
+	}
+	auto& result = context.results->try_emplace(key, this->current_output(index, *context.delays))
+					   .first->second; // Use a reference to avoid potential iterator invalidation caused by evaluate_output_impl.
+	auto const value = this->evaluate_output_impl(index, context);
+	ASIC_ASSERT(&context.results->at(key) == &result);
+	result = value;
+	return value;
+}
+
+number abstract_operation::truncate_input(std::size_t index, number value, std::size_t bits) const {
+	if (value.imag() != 0) {
+		throw py::type_error{
+			fmt::format("Complex value cannot be truncated to {} bits as requested by the signal connected to input #{}", bits, index)};
+	}
+	if (bits > 64) {
+		throw py::value_error{
+			fmt::format("Cannot truncate to {} (more than 64) bits as requested by the singal connected to input #{}", bits, index)};
+	}
+	return number{static_cast<number::value_type>(static_cast<std::int64_t>(value.real()) & ((std::int64_t{1} << bits) - 1))};
+}
+
+result_key const& abstract_operation::key_base() const {
+	return m_key;
+}
+
+result_key abstract_operation::key_of_output(std::size_t index) const {
+	if (m_key.empty()) {
+		return fmt::to_string(index);
+	}
+	if (this->output_count() == 1) {
+		return m_key;
+	}
+	return fmt::format("{}.{}", m_key, index);
+}
+
+unary_operation::unary_operation(result_key key)
+	: abstract_operation(std::move(key)) {}
+
+void unary_operation::connect(signal_source in) {
+	m_in = std::move(in);
+}
+
+bool unary_operation::connected() const noexcept {
+	return static_cast<bool>(m_in);
+}
+
+signal_source const& unary_operation::input() const noexcept {
+	return m_in;
+}
+
+number unary_operation::evaluate_input(evaluation_context const& context) const {
+	auto const value = m_in.evaluate_output(context);
+	auto const bits = context.bits_override.value_or(m_in.bits().value_or(0));
+	return (context.truncate && bits) ? this->truncate_input(0, value, bits) : value;
+}
+
+binary_operation::binary_operation(result_key key)
+	: abstract_operation(std::move(key)) {}
+
+void binary_operation::connect(signal_source lhs, signal_source rhs) {
+	m_lhs = std::move(lhs);
+	m_rhs = std::move(rhs);
+}
+
+signal_source const& binary_operation::lhs() const noexcept {
+	return m_lhs;
+}
+
+signal_source const& binary_operation::rhs() const noexcept {
+	return m_rhs;
+}
+
+number binary_operation::evaluate_lhs(evaluation_context const& context) const {
+	auto const value = m_lhs.evaluate_output(context);
+	auto const bits = context.bits_override.value_or(m_lhs.bits().value_or(0));
+	return (context.truncate && bits) ? this->truncate_input(0, value, bits) : value;
+}
+
+number binary_operation::evaluate_rhs(evaluation_context const& context) const {
+	auto const value = m_rhs.evaluate_output(context);
+	auto const bits = context.bits_override.value_or(m_rhs.bits().value_or(0));
+	return (context.truncate && bits) ? this->truncate_input(0, value, bits) : value;
+}
+
+nary_operation::nary_operation(result_key key)
+	: abstract_operation(std::move(key)) {}
+
+void nary_operation::connect(std::vector<signal_source> inputs) {
+	m_inputs = std::move(inputs);
+}
+
+span<signal_source const> nary_operation::inputs() const noexcept {
+	return m_inputs;
+}
+
+std::vector<number> nary_operation::evaluate_inputs(evaluation_context const& context) const {
+	auto values = std::vector<number>{};
+	values.reserve(m_inputs.size());
+	for (auto const& input : m_inputs) {
+		auto const value = input.evaluate_output(context);
+		auto const bits = context.bits_override.value_or(input.bits().value_or(0));
+		values.push_back((context.truncate && bits) ? this->truncate_input(0, value, bits) : value);
+	}
+	return values;
+}
+
+} // namespace asic
\ No newline at end of file
diff --git a/legacy/simulation_oop/operation.h b/legacy/simulation_oop/operation.h
new file mode 100644
index 0000000000000000000000000000000000000000..344eacc1482c40021b3d0ff686cbef5c71085f58
--- /dev/null
+++ b/legacy/simulation_oop/operation.h
@@ -0,0 +1,132 @@
+#ifndef ASIC_SIMULATION_OPERATION_H
+#define ASIC_SIMULATION_OPERATION_H
+
+#include "../number.h"
+#include "../span.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <fmt/format.h>
+#include <memory>
+#include <optional>
+#include <stdexcept>
+#include <string>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+namespace asic {
+
+class operation;
+class signal_source;
+
+using result_key = std::string;
+using result_map = std::unordered_map<result_key, std::optional<number>>;
+using delay_map = std::unordered_map<result_key, number>;
+using delay_queue = std::vector<std::pair<result_key, signal_source const*>>;
+
+struct evaluation_context final {
+	result_map* results;
+	delay_map* delays;
+	delay_queue* deferred_delays;
+	std::optional<std::size_t> bits_override;
+	bool truncate;
+};
+
+class signal_source final {
+public:
+	signal_source() noexcept = default;
+	signal_source(std::shared_ptr<const operation> op, std::size_t index, std::optional<std::size_t> bits);
+
+	[[nodiscard]] explicit operator bool() const noexcept;
+
+	[[nodiscard]] std::optional<number> current_output(delay_map const& delays) const;
+	[[nodiscard]] number evaluate_output(evaluation_context const& context) const;
+
+	[[nodiscard]] std::optional<std::size_t> bits() const noexcept;
+
+private:
+	std::shared_ptr<const operation> m_operation;
+	std::size_t m_index = 0;
+	std::optional<std::size_t> m_bits;
+};
+
+class operation {
+public:
+	virtual ~operation() = default;
+	[[nodiscard]] virtual std::size_t output_count() const noexcept = 0;
+	[[nodiscard]] virtual std::optional<number> current_output(std::size_t index, delay_map const& delays) const = 0;
+	[[nodiscard]] virtual number evaluate_output(std::size_t index, evaluation_context const& context) const = 0;
+};
+
+class abstract_operation : public operation {
+public:
+	explicit abstract_operation(result_key key);
+	virtual ~abstract_operation() = default;
+
+	[[nodiscard]] std::optional<number> current_output(std::size_t, delay_map const&) const override;
+	[[nodiscard]] number evaluate_output(std::size_t index, evaluation_context const& context) const override;
+
+protected:
+	[[nodiscard]] virtual number evaluate_output_impl(std::size_t index, evaluation_context const& context) const = 0;
+	[[nodiscard]] virtual number truncate_input(std::size_t index, number value, std::size_t bits) const;
+
+	[[nodiscard]] result_key const& key_base() const;
+	[[nodiscard]] result_key key_of_output(std::size_t index) const;
+
+private:
+	result_key m_key;
+};
+
+class unary_operation : public abstract_operation {
+public:
+	explicit unary_operation(result_key key);
+	virtual ~unary_operation() = default;
+
+	void connect(signal_source in);
+
+protected:
+	[[nodiscard]] bool connected() const noexcept;
+	[[nodiscard]] signal_source const& input() const noexcept;
+	[[nodiscard]] number evaluate_input(evaluation_context const& context) const;
+
+private:
+	signal_source m_in;
+};
+
+class binary_operation : public abstract_operation {
+public:
+	explicit binary_operation(result_key key);
+	virtual ~binary_operation() = default;
+
+	void connect(signal_source lhs, signal_source rhs);
+
+protected:
+	[[nodiscard]] signal_source const& lhs() const noexcept;
+	[[nodiscard]] signal_source const& rhs() const noexcept;
+	[[nodiscard]] number evaluate_lhs(evaluation_context const& context) const;
+	[[nodiscard]] number evaluate_rhs(evaluation_context const& context) const;
+
+private:
+	signal_source m_lhs;
+	signal_source m_rhs;
+};
+
+class nary_operation : public abstract_operation {
+public:
+	explicit nary_operation(result_key key);
+	virtual ~nary_operation() = default;
+
+	void connect(std::vector<signal_source> inputs);
+
+protected:
+	[[nodiscard]] span<signal_source const> inputs() const noexcept;
+	[[nodiscard]] std::vector<number> evaluate_inputs(evaluation_context const& context) const;
+
+private:
+	std::vector<signal_source> m_inputs;
+};
+
+} // namespace asic
+
+#endif // ASIC_SIMULATION_OPERATION_H
\ No newline at end of file
diff --git a/legacy/simulation_oop/signal_flow_graph.cpp b/legacy/simulation_oop/signal_flow_graph.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..4c3763c81e8f97f22caf42a47d88c1186b9a874d
--- /dev/null
+++ b/legacy/simulation_oop/signal_flow_graph.cpp
@@ -0,0 +1,144 @@
+#include "signal_flow_graph.h"
+
+#include "../debug.h"
+
+namespace py = pybind11;
+
+namespace asic {
+
+signal_flow_graph_operation::signal_flow_graph_operation(result_key key)
+	: abstract_operation(std::move(key)) {}
+
+void signal_flow_graph_operation::create(pybind11::handle sfg, added_operation_cache& added) {
+	ASIC_DEBUG_MSG("Creating SFG.");
+	for (auto const& [i, op] : enumerate(sfg.attr("output_operations"))) {
+		ASIC_DEBUG_MSG("Adding output op.");
+		m_output_operations.emplace_back(this->key_of_output(i)).connect(make_source(op, 0, added, this->key_base()));
+	}
+	for (auto const& op : sfg.attr("input_operations")) {
+		ASIC_DEBUG_MSG("Adding input op.");
+		if (!m_input_operations.emplace_back(std::dynamic_pointer_cast<input_operation>(make_operation(op, added, this->key_base())))) {
+			throw py::value_error{"Invalid input operation in SFG."};
+		}
+	}
+}
+
+std::vector<std::shared_ptr<input_operation>> const& signal_flow_graph_operation::inputs() noexcept {
+	return m_input_operations;
+}
+
+std::size_t signal_flow_graph_operation::output_count() const noexcept {
+	return m_output_operations.size();
+}
+
+number signal_flow_graph_operation::evaluate_output(std::size_t index, evaluation_context const& context) const {
+	ASIC_DEBUG_MSG("Evaluating SFG.");
+	return m_output_operations.at(index).evaluate_output(0, context);
+}
+
+number signal_flow_graph_operation::evaluate_output_impl(std::size_t, evaluation_context const&) const {
+	return number{};
+}
+
+signal_source signal_flow_graph_operation::make_source(pybind11::handle op, std::size_t input_index, added_operation_cache& added,
+													   std::string_view prefix) {
+	auto const signal = py::object{op.attr("inputs")[py::int_{input_index}].attr("signals")[py::int_{0}]};
+	auto const src = py::handle{signal.attr("source")};
+	auto const operation = py::handle{src.attr("operation")};
+	auto const index = src.attr("index").cast<std::size_t>();
+	auto bits = std::optional<std::size_t>{};
+	if (!signal.attr("bits").is_none()) {
+		bits = signal.attr("bits").cast<std::size_t>();
+	}
+	return signal_source{make_operation(operation, added, prefix), index, bits};
+}
+
+std::shared_ptr<operation> signal_flow_graph_operation::add_signal_flow_graph_operation(pybind11::handle sfg, added_operation_cache& added,
+																						std::string_view prefix, result_key key) {
+	auto const new_op = add_operation<signal_flow_graph_operation>(sfg, added, std::move(key));
+	new_op->create(sfg, added);
+	for (auto&& [i, input] : enumerate(new_op->inputs())) {
+		input->connect(make_source(sfg, i, added, prefix));
+	}
+	return new_op;
+}
+
+std::shared_ptr<custom_operation> signal_flow_graph_operation::add_custom_operation(pybind11::handle op, added_operation_cache& added,
+																					std::string_view prefix, result_key key) {
+	auto const input_count = op.attr("input_count").cast<std::size_t>();
+	auto const output_count = op.attr("output_count").cast<std::size_t>();
+	auto const new_op = add_operation<custom_operation>(
+		op, added, key, op.attr("evaluate_output"), op.attr("truncate_input"), output_count);
+	auto inputs = std::vector<signal_source>{};
+	inputs.reserve(input_count);
+	for (auto const i : range(input_count)) {
+		inputs.push_back(make_source(op, i, added, prefix));
+	}
+	new_op->connect(std::move(inputs));
+	return new_op;
+}
+
+std::shared_ptr<operation> signal_flow_graph_operation::make_operation(pybind11::handle op, added_operation_cache& added,
+																	   std::string_view prefix) {
+	if (auto const it = added.find(op.ptr()); it != added.end()) {
+		ASIC_ASSERT(it->second);
+		return it->second;
+	}
+	auto const graph_id = op.attr("graph_id").cast<std::string_view>();
+	auto const type_name = op.attr("type_name")().cast<std::string_view>();
+	auto key = (prefix.empty()) ? result_key{graph_id} : fmt::format("{}.{}", prefix, graph_id);
+	if (type_name == "c") {
+		auto const value = op.attr("value").cast<number>();
+		return add_operation<constant_operation>(op, added, std::move(key), value);
+	}
+	if (type_name == "add") {
+		return add_binary_operation<addition_operation>(op, added, prefix, std::move(key));
+	}
+	if (type_name == "sub") {
+		return add_binary_operation<subtraction_operation>(op, added, prefix, std::move(key));
+	}
+	if (type_name == "mul") {
+		return add_binary_operation<multiplication_operation>(op, added, prefix, std::move(key));
+	}
+	if (type_name == "div") {
+		return add_binary_operation<division_operation>(op, added, prefix, std::move(key));
+	}
+	if (type_name == "min") {
+		return add_binary_operation<min_operation>(op, added, prefix, std::move(key));
+	}
+	if (type_name == "max") {
+		return add_binary_operation<max_operation>(op, added, prefix, std::move(key));
+	}
+	if (type_name == "sqrt") {
+		return add_unary_operation<square_root_operation>(op, added, prefix, std::move(key));
+	}
+	if (type_name == "conj") {
+		return add_unary_operation<complex_conjugate_operation>(op, added, prefix, std::move(key));
+	}
+	if (type_name == "abs") {
+		return add_unary_operation<absolute_operation>(op, added, prefix, std::move(key));
+	}
+	if (type_name == "cmul") {
+		auto const value = op.attr("value").cast<number>();
+		return add_unary_operation<constant_multiplication_operation>(op, added, prefix, std::move(key), value);
+	}
+	if (type_name == "bfly") {
+		return add_binary_operation<butterfly_operation>(op, added, prefix, std::move(key));
+	}
+	if (type_name == "in") {
+		return add_operation<input_operation>(op, added, std::move(key));
+	}
+	if (type_name == "out") {
+		return add_unary_operation<output_operation>(op, added, prefix, std::move(key));
+	}
+	if (type_name == "t") {
+		auto const initial_value = op.attr("initial_value").cast<number>();
+		return add_unary_operation<delay_operation>(op, added, prefix, std::move(key), initial_value);
+	}
+	if (type_name == "sfg") {
+		return add_signal_flow_graph_operation(op, added, prefix, std::move(key));
+	}
+	return add_custom_operation(op, added, prefix, std::move(key));
+}
+
+} // namespace asic
\ No newline at end of file
diff --git a/legacy/simulation_oop/signal_flow_graph.h b/legacy/simulation_oop/signal_flow_graph.h
new file mode 100644
index 0000000000000000000000000000000000000000..f06788249e367d3e8e0602f04c6dcf91c71b7a96
--- /dev/null
+++ b/legacy/simulation_oop/signal_flow_graph.h
@@ -0,0 +1,82 @@
+#ifndef ASIC_SIMULATION_SIGNAL_FLOW_GRAPH_H
+#define ASIC_SIMULATION_SIGNAL_FLOW_GRAPH_H
+
+#include "../algorithm.h"
+#include "../debug.h"
+#include "../number.h"
+#include "core_operations.h"
+#include "custom_operation.h"
+#include "operation.h"
+#include "special_operations.h"
+
+#include <Python.h>
+#include <cstddef>
+#include <fmt/format.h>
+#include <functional>
+#include <memory>
+#include <pybind11/pybind11.h>
+#include <stdexcept>
+#include <string_view>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+namespace asic {
+
+class signal_flow_graph_operation final : public abstract_operation {
+public:
+	using added_operation_cache = std::unordered_map<PyObject const*, std::shared_ptr<operation>>;
+
+	signal_flow_graph_operation(result_key key);
+
+	void create(pybind11::handle sfg, added_operation_cache& added);
+
+	[[nodiscard]] std::vector<std::shared_ptr<input_operation>> const& inputs() noexcept;
+	[[nodiscard]] std::size_t output_count() const noexcept final;
+
+	[[nodiscard]] number evaluate_output(std::size_t index, evaluation_context const& context) const final;
+
+private:
+	[[nodiscard]] number evaluate_output_impl(std::size_t index, evaluation_context const& context) const final;
+
+	[[nodiscard]] static signal_source make_source(pybind11::handle op, std::size_t input_index, added_operation_cache& added,
+												   std::string_view prefix);
+
+	template <typename Operation, typename... Args>
+	[[nodiscard]] static std::shared_ptr<Operation> add_operation(pybind11::handle op, added_operation_cache& added, Args&&... args) {
+		return std::static_pointer_cast<Operation>(
+			added.try_emplace(op.ptr(), std::make_shared<Operation>(std::forward<Args>(args)...)).first->second);
+	}
+
+	template <typename Operation, typename... Args>
+	[[nodiscard]] static std::shared_ptr<Operation> add_unary_operation(pybind11::handle op, added_operation_cache& added,
+																		std::string_view prefix, Args&&... args) {
+		auto const new_op = add_operation<Operation>(op, added, std::forward<Args>(args)...);
+		new_op->connect(make_source(op, 0, added, prefix));
+		return new_op;
+	}
+
+	template <typename Operation, typename... Args>
+	[[nodiscard]] static std::shared_ptr<Operation> add_binary_operation(pybind11::handle op, added_operation_cache& added,
+																		 std::string_view prefix, Args&&... args) {
+		auto const new_op = add_operation<Operation>(op, added, std::forward<Args>(args)...);
+		new_op->connect(make_source(op, 0, added, prefix), make_source(op, 1, added, prefix));
+		return new_op;
+	}
+
+	[[nodiscard]] static std::shared_ptr<operation> add_signal_flow_graph_operation(pybind11::handle sfg, added_operation_cache& added,
+																					std::string_view prefix, result_key key);
+
+	[[nodiscard]] static std::shared_ptr<custom_operation> add_custom_operation(pybind11::handle op, added_operation_cache& added,
+																				std::string_view prefix, result_key key);
+
+	[[nodiscard]] static std::shared_ptr<operation> make_operation(pybind11::handle op, added_operation_cache& added,
+																   std::string_view prefix);
+
+	std::vector<output_operation> m_output_operations;
+	std::vector<std::shared_ptr<input_operation>> m_input_operations;
+};
+
+} // namespace asic
+
+#endif // ASIC_SIMULATION_SIGNAL_FLOW_GRAPH_H
\ No newline at end of file
diff --git a/legacy/simulation_oop/simulation.cpp b/legacy/simulation_oop/simulation.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..4d6ff83337a6dade0a0d476e5942ee3b14178195
--- /dev/null
+++ b/legacy/simulation_oop/simulation.cpp
@@ -0,0 +1,138 @@
+#define NOMINMAX
+#include "simulation.h"
+
+#include "../debug.h"
+
+namespace py = pybind11;
+
+namespace asic {
+
+simulation::simulation(pybind11::handle sfg, std::optional<std::vector<std::optional<input_provider_t>>> input_providers)
+	: m_sfg("")
+	, m_input_functions(sfg.attr("input_count").cast<std::size_t>(), [](iteration_t) -> number { return number{}; }) {
+	if (input_providers) {
+		this->set_inputs(std::move(*input_providers));
+	}
+	auto added = signal_flow_graph_operation::added_operation_cache{};
+	m_sfg.create(sfg, added);
+}
+
+void simulation::set_input(std::size_t index, input_provider_t input_provider) {
+	if (index >= m_input_functions.size()) {
+		throw py::index_error{fmt::format("Input index out of range (expected 0-{}, got {})", m_input_functions.size() - 1, index)};
+	}
+	if (auto* const callable = std::get_if<input_function_t>(&input_provider)) {
+		m_input_functions[index] = std::move(*callable);
+	} else if (auto* const numeric = std::get_if<number>(&input_provider)) {
+		m_input_functions[index] = [value = *numeric](iteration_t) -> number {
+			return value;
+		};
+	} else if (auto* const list = std::get_if<std::vector<number>>(&input_provider)) {
+		if (!m_input_length) {
+			m_input_length = static_cast<iteration_t>(list->size());
+		} else if (*m_input_length != static_cast<iteration_t>(list->size())) {
+			throw py::value_error{fmt::format("Inconsistent input length for simulation (was {}, got {})", *m_input_length, list->size())};
+		}
+		m_input_functions[index] = [values = std::move(*list)](iteration_t n) -> number {
+			return values.at(n);
+		};
+	}
+}
+
+void simulation::set_inputs(std::vector<std::optional<input_provider_t>> input_providers) {
+	if (input_providers.size() != m_input_functions.size()) {
+		throw py::value_error{fmt::format(
+			"Wrong number of inputs supplied to simulation (expected {}, got {})", m_input_functions.size(), input_providers.size())};
+	}
+	for (auto&& [i, input_provider] : enumerate(input_providers)) {
+		if (input_provider) {
+			this->set_input(i, std::move(*input_provider));
+		}
+	}
+}
+
+std::vector<number> simulation::step(bool save_results, std::optional<std::size_t> bits_override, bool truncate) {
+	return this->run_for(1, save_results, bits_override, truncate);
+}
+
+std::vector<number> simulation::run_until(iteration_t iteration, bool save_results, std::optional<std::size_t> bits_override,
+										  bool truncate) {
+	auto result = std::vector<number>{};
+	while (m_iteration < iteration) {
+		ASIC_DEBUG_MSG("Running simulation iteration.");
+		for (auto&& [input, function] : zip(m_sfg.inputs(), m_input_functions)) {
+			input->value(function(m_iteration));
+		}
+
+		result.clear();
+		result.reserve(m_sfg.output_count());
+
+		auto results = result_map{};
+		auto deferred_delays = delay_queue{};
+		auto context = evaluation_context{};
+		context.results = &results;
+		context.delays = &m_delays;
+		context.deferred_delays = &deferred_delays;
+		context.bits_override = bits_override;
+		context.truncate = truncate;
+
+		for (auto const i : range(m_sfg.output_count())) {
+			result.push_back(m_sfg.evaluate_output(i, context));
+		}
+
+		while (!deferred_delays.empty()) {
+			auto new_deferred_delays = delay_queue{};
+			context.deferred_delays = &new_deferred_delays;
+			for (auto const& [key, src] : deferred_delays) {
+				ASIC_ASSERT(src);
+				m_delays[key] = src->evaluate_output(context);
+			}
+			deferred_delays = std::move(new_deferred_delays);
+		}
+
+		if (save_results) {
+			for (auto const& [key, value] : results) {
+				m_results[key].push_back(value.value());
+			}
+		}
+		++m_iteration;
+	}
+	return result;
+}
+
+std::vector<number> simulation::run_for(iteration_t iterations, bool save_results, std::optional<std::size_t> bits_override,
+										bool truncate) {
+	if (iterations > std::numeric_limits<iteration_t>::max() - m_iteration) {
+		throw py::value_error("Simulation iteration type overflow!");
+	}
+	return this->run_until(m_iteration + iterations, save_results, bits_override, truncate);
+}
+
+std::vector<number> simulation::run(bool save_results, std::optional<std::size_t> bits_override, bool truncate) {
+	if (m_input_length) {
+		return this->run_until(*m_input_length, save_results, bits_override, truncate);
+	}
+	throw py::index_error{"Tried to run unlimited simulation"};
+}
+
+iteration_t simulation::iteration() const noexcept {
+	return m_iteration;
+}
+
+pybind11::dict simulation::results() const noexcept {
+	auto results = py::dict{};
+	for (auto const& [key, values] : m_results) {
+		results[py::str{key}] = py::array{static_cast<py::ssize_t>(values.size()), values.data()};
+	}
+	return results;
+}
+
+void simulation::clear_results() noexcept {
+	m_results.clear();
+}
+
+void simulation::clear_state() noexcept {
+	m_delays.clear();
+}
+
+} // namespace asic
diff --git a/legacy/simulation_oop/simulation.h b/legacy/simulation_oop/simulation.h
new file mode 100644
index 0000000000000000000000000000000000000000..38e2771b877772bd28048cae16d791bb4e0b45e3
--- /dev/null
+++ b/legacy/simulation_oop/simulation.h
@@ -0,0 +1,66 @@
+#ifndef ASIC_SIMULATION_OOP_H
+#define ASIC_SIMULATION_OOP_H
+
+#include "../number.h"
+#include "core_operations.h"
+#include "custom_operation.h"
+#include "operation.h"
+#include "signal_flow_graph.h"
+#include "special_operations.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <fmt/format.h>
+#include <functional>
+#include <limits>
+#include <memory>
+#include <optional>
+#include <pybind11/functional.h>
+#include <pybind11/numpy.h>
+#include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+#include <string_view>
+#include <unordered_map>
+#include <utility>
+#include <variant>
+#include <vector>
+
+namespace asic {
+
+using iteration_t = std::uint32_t;
+using result_array_map = std::unordered_map<std::string, std::vector<number>>;
+using input_function_t = std::function<number(iteration_t)>;
+using input_provider_t = std::variant<number, std::vector<number>, input_function_t>;
+
+class simulation final {
+public:
+	simulation(pybind11::handle sfg, std::optional<std::vector<std::optional<input_provider_t>>> input_providers = std::nullopt);
+
+	void set_input(std::size_t index, input_provider_t input_provider);
+	void set_inputs(std::vector<std::optional<input_provider_t>> input_providers);
+
+	[[nodiscard]] std::vector<number> step(bool save_results, std::optional<std::size_t> bits_override, bool truncate);
+	[[nodiscard]] std::vector<number> run_until(iteration_t iteration, bool save_results, std::optional<std::size_t> bits_override,
+												bool truncate);
+	[[nodiscard]] std::vector<number> run_for(iteration_t iterations, bool save_results, std::optional<std::size_t> bits_override,
+											  bool truncate);
+	[[nodiscard]] std::vector<number> run(bool save_results, std::optional<std::size_t> bits_override, bool truncate);
+
+	[[nodiscard]] iteration_t iteration() const noexcept;
+	[[nodiscard]] pybind11::dict results() const noexcept;
+
+	void clear_results() noexcept;
+	void clear_state() noexcept;
+
+private:
+	signal_flow_graph_operation m_sfg;
+	result_array_map m_results;
+	delay_map m_delays;
+	iteration_t m_iteration = 0;
+	std::vector<input_function_t> m_input_functions;
+	std::optional<iteration_t> m_input_length;
+};
+
+} // namespace asic
+
+#endif // ASIC_SIMULATION_OOP_H
\ No newline at end of file
diff --git a/legacy/simulation_oop/special_operations.cpp b/legacy/simulation_oop/special_operations.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1f7a6519a90ba08224585e36093694becf495c4d
--- /dev/null
+++ b/legacy/simulation_oop/special_operations.cpp
@@ -0,0 +1,78 @@
+#include "special_operations.h"
+
+#include "../debug.h"
+
+namespace asic {
+
+input_operation::input_operation(result_key key)
+	: unary_operation(std::move(key)) {}
+
+std::size_t input_operation::output_count() const noexcept {
+	return 1;
+}
+
+number input_operation::value() const noexcept {
+	return m_value;
+}
+
+void input_operation::value(number value) noexcept {
+	m_value = value;
+}
+
+number input_operation::evaluate_output_impl(std::size_t, evaluation_context const& context) const {
+	ASIC_DEBUG_MSG("Evaluating input.");
+	if (this->connected()) {
+		return this->evaluate_input(context);
+	}
+	return m_value;
+}
+
+output_operation::output_operation(result_key key)
+	: unary_operation(std::move(key)) {}
+
+std::size_t output_operation::output_count() const noexcept {
+	return 1;
+}
+
+number output_operation::evaluate_output_impl(std::size_t, evaluation_context const& context) const {
+	ASIC_DEBUG_MSG("Evaluating output.");
+	return this->evaluate_input(context);
+}
+
+delay_operation::delay_operation(result_key key, number initial_value)
+	: unary_operation(std::move(key))
+	, m_initial_value(initial_value) {}
+
+std::size_t delay_operation::output_count() const noexcept {
+	return 1;
+}
+
+std::optional<number> delay_operation::current_output(std::size_t index, delay_map const& delays) const {
+	auto const key = this->key_of_output(index);
+	if (auto const it = delays.find(key); it != delays.end()) {
+		return it->second;
+	}
+	return m_initial_value;
+}
+
+number delay_operation::evaluate_output(std::size_t index, evaluation_context const& context) const {
+	ASIC_DEBUG_MSG("Evaluating delay.");
+	ASIC_ASSERT(index == 0);
+	ASIC_ASSERT(context.results);
+	ASIC_ASSERT(context.delays);
+	ASIC_ASSERT(context.deferred_delays);
+	auto key = this->key_of_output(index);
+	auto const value = context.delays->try_emplace(key, m_initial_value).first->second;
+	auto const& [it, inserted] = context.results->try_emplace(key, value);
+	if (inserted) {
+		context.deferred_delays->emplace_back(std::move(key), &this->input());
+		return value;
+	}
+	return it->second.value();
+}
+
+[[nodiscard]] number delay_operation::evaluate_output_impl(std::size_t, evaluation_context const&) const {
+	return number{};
+}
+
+} // namespace asic
diff --git a/legacy/simulation_oop/special_operations.h b/legacy/simulation_oop/special_operations.h
new file mode 100644
index 0000000000000000000000000000000000000000..88fb087e84378e36f423364d2c7d83a083828784
--- /dev/null
+++ b/legacy/simulation_oop/special_operations.h
@@ -0,0 +1,55 @@
+#ifndef ASIC_SIMULATION_SPECIAL_OPERATIONS_H
+#define ASIC_SIMULATION_SPECIAL_OPERATIONS_H
+
+#include "../debug.h"
+#include "../number.h"
+#include "operation.h"
+
+#include <cassert>
+#include <cstddef>
+#include <utility>
+
+namespace asic {
+
+class input_operation final : public unary_operation {
+public:
+	explicit input_operation(result_key key);
+
+	[[nodiscard]] std::size_t output_count() const noexcept final;
+	[[nodiscard]] number value() const noexcept;
+	void value(number value) noexcept;
+
+private:
+	[[nodiscard]] number evaluate_output_impl(std::size_t index, evaluation_context const& context) const final;
+
+	number m_value{};
+};
+
+class output_operation final : public unary_operation {
+public:
+	explicit output_operation(result_key key);
+
+	[[nodiscard]] std::size_t output_count() const noexcept final;
+
+private:
+	[[nodiscard]] number evaluate_output_impl(std::size_t index, evaluation_context const& context) const final;
+};
+
+class delay_operation final : public unary_operation {
+public:
+	delay_operation(result_key key, number initial_value);
+
+	[[nodiscard]] std::size_t output_count() const noexcept final;
+
+	[[nodiscard]] std::optional<number> current_output(std::size_t index, delay_map const& delays) const final;
+	[[nodiscard]] number evaluate_output(std::size_t index, evaluation_context const& context) const final;
+
+private:
+	[[nodiscard]] number evaluate_output_impl(std::size_t index, evaluation_context const& context) const final;
+
+	number m_initial_value;
+};
+
+} // namespace asic
+
+#endif // ASIC_SIMULATION_SPECIAL_OPERATIONS_H
\ No newline at end of file
diff --git a/logo_tiny.png b/logo_tiny.png
new file mode 100644
index 0000000000000000000000000000000000000000..ae5359b69cdb0b04f1be87da750bccc9fbe16ac2
Binary files /dev/null and b/logo_tiny.png differ
diff --git a/setup.py b/setup.py
index 43d55d40a95212196facb973ebc97a1bdc5e7f42..e58603441c1c1c95a5870ce79f7a660fe025eaad 100644
--- a/setup.py
+++ b/setup.py
@@ -6,25 +6,30 @@ import setuptools
 from setuptools import Extension
 from setuptools.command.build_ext import build_ext
 
-CMAKE_EXE = os.environ.get('CMAKE_EXE', shutil.which('cmake'))
+CMAKE_EXE = os.environ.get("CMAKE_EXE", shutil.which("cmake"))
+
 
 class CMakeExtension(Extension):
-    def __init__(self, name, sourcedir = ""):
+    def __init__(self, name, sourcedir=""):
         super().__init__(name, sources=[])
         self.sourcedir = os.path.abspath(sourcedir)
 
+
 class CMakeBuild(build_ext):
     def build_extension(self, ext):
         if not isinstance(ext, CMakeExtension):
             return super().build_extension(ext)
 
         if not CMAKE_EXE:
-            raise RuntimeError(f"Cannot build extension {ext.name}: CMake executable not found! Set the CMAKE_EXE environment variable or update your path.")
+            raise RuntimeError(
+                f"Cannot build extension {ext.name}: CMake executable not found! Set the CMAKE_EXE environment variable or update your path.")
 
         cmake_build_type = "Debug" if self.debug else "Release"
-        cmake_output_dir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name)))
+        cmake_output_dir = os.path.abspath(
+            os.path.dirname(self.get_ext_fullpath(ext.name)))
         cmake_configure_argv = [
             CMAKE_EXE, ext.sourcedir,
+            "-DASIC_BUILDING_PYTHON_DISTRIBUTION=true",
             "-DCMAKE_BUILD_TYPE=" + cmake_build_type,
             "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=" + cmake_output_dir,
             "-DPYTHON_EXECUTABLE=" + sys.executable,
@@ -36,13 +41,14 @@ class CMakeBuild(build_ext):
 
         if not os.path.exists(self.build_temp):
             os.makedirs(self.build_temp)
-        
+
         env = os.environ.copy()
-        
+
         print(f"=== Configuring {ext.name} ===")
         print(f"Temp dir: {self.build_temp}")
         print(f"Output dir: {cmake_output_dir}")
-        subprocess.check_call(cmake_configure_argv, cwd=self.build_temp, env=env)
+        subprocess.check_call(cmake_configure_argv,
+                              cwd=self.build_temp, env=env)
 
         print(f"=== Building {ext.name} ===")
         print(f"Temp dir: {self.build_temp}")
@@ -52,29 +58,33 @@ class CMakeBuild(build_ext):
 
         print()
 
+
 setuptools.setup(
-    name = "b-asic",
-    version = "0.0.1",
-    author = "Adam Jakobsson, Angus Lothian, Arvid Westerlund, Felix Goding, Ivar Härnqvist, Jacob Wahlman, Kevin Scott, Rasmus Karlsson",
-    author_email = "adaja901@student.liu.se, anglo547@student.liu.se, arvwe160@student.liu.se, felgo673@student.liu.se, ivaha717@student.liu.se, jacwa448@student.liu.se, kevsc634@student.liu.se, raska119@student.liu.se",
-    description = "Better ASIC Toolbox",
-    long_description = open("README.md", "r").read(),
-    long_description_content_type = "text/markdown",
-    url = "https://gitlab.liu.se/PUM_TDDD96/B-ASIC",
-    license = "MIT",
-    classifiers = [
+    name="b-asic",
+    version="1.0.0",
+    author="Adam Jakobsson, Angus Lothian, Arvid Westerlund, Felix Goding, Ivar Härnqvist, Jacob Wahlman, Kevin Scott, Rasmus Karlsson",
+    author_email="adaja901@student.liu.se, anglo547@student.liu.se, arvwe160@student.liu.se, felgo673@student.liu.se, ivaha717@student.liu.se, jacwa448@student.liu.se, kevsc634@student.liu.se, raska119@student.liu.se",
+    description="Better ASIC Toolbox",
+    long_description=open("README.md", "r").read(),
+    long_description_content_type="text/markdown",
+    url="https://gitlab.liu.se/PUM_TDDD96/B-ASIC",
+    license="MIT",
+    classifiers=[
         "Programming Language :: Python :: 3",
         "License :: OSI Approved :: MIT License",
         "Operating System :: OS Independent",
     ],
-    python_requires = ">=3.6",
-    install_requires = [
+    python_requires=">=3.6",
+    install_requires=[
         "pybind11>=2.3.0",
         "numpy",
-        "install_qt_binding"
+        "pyside2",
+        "graphviz",
+        "matplotlib"
     ],
-    packages = ["b_asic"],
-    ext_modules = [CMakeExtension("b_asic")],
-    cmdclass = {"build_ext": CMakeBuild},
-    zip_safe = False
-)
\ No newline at end of file
+    packages=["b_asic", "b_asic/GUI"],
+    ext_modules=[CMakeExtension("b_asic")],
+    cmdclass={"build_ext": CMakeBuild},
+    zip_safe=False,
+    include_package_data=True
+)
diff --git a/small_logo.png b/small_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..689a38192b9fc4c6ed490e1e0001fd5dd264c968
Binary files /dev/null and b/small_logo.png differ
diff --git a/src/algorithm.h b/src/algorithm.h
new file mode 100644
index 0000000000000000000000000000000000000000..c86275d1c4ef09a525372d38a4c07a0beb11e8c4
--- /dev/null
+++ b/src/algorithm.h
@@ -0,0 +1,325 @@
+#ifndef ASIC_ALGORITHM_H
+#define ASIC_ALGORITHM_H
+
+#include <cstddef>
+#include <iterator>
+#include <memory>
+#include <type_traits>
+#include <utility>
+
+namespace asic {
+namespace detail {
+
+template <typename Reference>
+class arrow_proxy final {
+public:
+	template <typename Ref>
+	constexpr explicit arrow_proxy(Ref&& r) : m_r(std::forward<Ref>(r)) {}
+
+	Reference* operator->() {
+		return std::addressof(m_r);
+	}
+
+private:
+	Reference m_r;
+};
+
+template <typename T>
+struct range_view final {
+	class iterator final {
+	public:
+		using difference_type = std::ptrdiff_t;
+		using value_type = T const;
+		using reference = value_type&;
+		using pointer = value_type*;
+		using iterator_category = std::random_access_iterator_tag;
+
+		constexpr iterator() noexcept = default;
+		constexpr explicit iterator(T value) noexcept : m_value(value) {}
+
+		[[nodiscard]] constexpr bool operator==(iterator const& other) const noexcept {
+			return m_value == other.m_value;
+		}
+
+		[[nodiscard]] constexpr bool operator!=(iterator const& other) const noexcept {
+			return m_value != other.m_value;
+		}
+
+		[[nodiscard]] constexpr bool operator<(iterator const& other) const noexcept {
+			return m_value < other.m_value;
+		}
+
+		[[nodiscard]] constexpr bool operator>(iterator const& other) const noexcept {
+			return m_value > other.m_value;
+		}
+
+		[[nodiscard]] constexpr bool operator<=(iterator const& other) const noexcept {
+			return m_value <= other.m_value;
+		}
+
+		[[nodiscard]] constexpr bool operator>=(iterator const& other) const noexcept {
+			return m_value >= other.m_value;
+		}
+
+		[[nodiscard]] constexpr reference operator*() const noexcept {
+			return m_value;
+		}
+
+		[[nodiscard]] constexpr pointer operator->() const noexcept {
+			return std::addressof(**this);
+		}
+
+		constexpr iterator& operator++() noexcept {
+			++m_value;
+			return *this;
+		}
+
+		constexpr iterator operator++(int) noexcept {
+			return iterator{m_value++};
+		}
+
+		constexpr iterator& operator--() noexcept {
+			--m_value;
+			return *this;
+		}
+
+		constexpr iterator operator--(int) noexcept {
+			return iterator{m_value--};
+		}
+
+		constexpr iterator& operator+=(difference_type n) noexcept {
+			m_value += n;
+			return *this;
+		}
+
+		constexpr iterator& operator-=(difference_type n) noexcept {
+			m_value -= n;
+			return *this;
+		}
+
+		[[nodiscard]] constexpr T operator[](difference_type n) noexcept {
+			return m_value + static_cast<T>(n);
+		}
+
+		[[nodiscard]] constexpr friend iterator operator+(iterator const& lhs, difference_type rhs) noexcept {
+			return iterator{lhs.m_value + rhs};
+		}
+
+		[[nodiscard]] constexpr friend iterator operator+(difference_type lhs, iterator const& rhs) noexcept {
+			return iterator{lhs + rhs.m_value};
+		}
+
+		[[nodiscard]] constexpr friend iterator operator-(iterator const& lhs, difference_type rhs) noexcept {
+			return iterator{lhs.m_value - rhs};
+		}
+
+		[[nodiscard]] constexpr friend difference_type operator-(iterator const& lhs, iterator const& rhs) noexcept {
+			return static_cast<difference_type>(lhs.m_value - rhs.m_value);
+		}
+
+	private:
+		T m_value{};
+	};
+
+	using sentinel = iterator;
+
+	template <typename First, typename Last>
+	constexpr range_view(First&& first, Last&& last) noexcept : m_begin(std::forward<First>(first)), m_end(std::forward<Last>(last)) {}
+
+	[[nodiscard]] constexpr iterator begin() const noexcept {
+		return m_begin;
+	}
+	[[nodiscard]] constexpr sentinel end() const noexcept {
+		return m_end;
+	}
+
+	iterator m_begin;
+	sentinel m_end;
+};
+
+template <typename Range, typename Iterator, typename Sentinel>
+struct enumerate_view final {
+	using sentinel = Sentinel;
+
+	class iterator final {
+	public:
+		using difference_type = typename std::iterator_traits<Iterator>::difference_type;
+		using value_type = typename std::iterator_traits<Iterator>::value_type;
+		using reference = std::pair<std::size_t const&, decltype(*std::declval<Iterator const>())>;
+		using pointer = arrow_proxy<reference>;
+		using iterator_category =
+			std::common_type_t<typename std::iterator_traits<Iterator>::iterator_category, std::bidirectional_iterator_tag>;
+
+		constexpr iterator() = default;
+
+		constexpr iterator(Iterator it, std::size_t index) : m_it(std::move(it)), m_index(index) {}
+
+		[[nodiscard]] constexpr bool operator==(iterator const& other) const {
+			return m_it == other.m_it;
+		}
+
+		[[nodiscard]] constexpr bool operator!=(iterator const& other) const {
+			return m_it != other.m_it;
+		}
+
+		[[nodiscard]] constexpr bool operator==(sentinel const& other) const {
+			return m_it == other;
+		}
+
+		[[nodiscard]] constexpr bool operator!=(sentinel const& other) const {
+			return m_it != other;
+		}
+
+		[[nodiscard]] constexpr reference operator*() const {
+			return reference{m_index, *m_it};
+		}
+
+		[[nodiscard]] constexpr pointer operator->() const {
+			return pointer{**this};
+		}
+
+		constexpr iterator& operator++() {
+			++m_it;
+			++m_index;
+			return *this;
+		}
+
+		constexpr iterator operator++(int) {
+			return iterator{m_it++, m_index++};
+		}
+
+		constexpr iterator& operator--() {
+			--m_it;
+			--m_index;
+			return *this;
+		}
+
+		constexpr iterator operator--(int) {
+			return iterator{m_it--, m_index--};
+		}
+
+	private:
+		Iterator m_it;
+		std::size_t m_index = 0;
+	};
+
+	constexpr iterator begin() const {
+		return iterator{std::begin(m_range), 0};
+	}
+
+	constexpr sentinel end() const {
+		return std::end(m_range);
+	}
+
+	Range m_range;
+};
+
+template <typename Range1, typename Range2, typename Iterator1, typename Iterator2, typename Sentinel1, typename Sentinel2>
+struct zip_view final {
+	using sentinel = std::pair<Sentinel1, Sentinel2>;
+
+	class iterator final {
+	public:
+		using difference_type = std::common_type_t<typename std::iterator_traits<Iterator1>::difference_type,
+			typename std::iterator_traits<Iterator2>::difference_type>;
+		using value_type =
+			std::pair<typename std::iterator_traits<Iterator1>::value_type, typename std::iterator_traits<Iterator2>::value_type>;
+		using reference = std::pair<decltype(*std::declval<Iterator1 const>()), decltype(*std::declval<Iterator2 const>())>;
+		using pointer = arrow_proxy<reference>;
+		using iterator_category = std::common_type_t<typename std::iterator_traits<Iterator1>::iterator_category,
+			typename std::iterator_traits<Iterator2>::iterator_category, std::bidirectional_iterator_tag>;
+
+		constexpr iterator() = default;
+
+		constexpr iterator(Iterator1 it1, Iterator2 it2) : m_it1(std::move(it1)), m_it2(std::move(it2)) {}
+
+		[[nodiscard]] constexpr bool operator==(iterator const& other) const {
+			return m_it1 == other.m_it1 && m_it2 == other.m_it2;
+		}
+
+		[[nodiscard]] constexpr bool operator!=(iterator const& other) const {
+			return !(*this == other);
+		}
+
+		[[nodiscard]] constexpr bool operator==(sentinel const& other) const {
+			return m_it1 == other.first || m_it2 == other.second;
+		}
+
+		[[nodiscard]] constexpr bool operator!=(sentinel const& other) const {
+			return !(*this == other);
+		}
+
+		[[nodiscard]] constexpr reference operator*() const {
+			return reference{*m_it1, *m_it2};
+		}
+
+		[[nodiscard]] constexpr pointer operator->() const {
+			return pointer{**this};
+		}
+
+		constexpr iterator& operator++() {
+			++m_it1;
+			++m_it2;
+			return *this;
+		}
+
+		constexpr iterator operator++(int) {
+			return iterator{m_it1++, m_it2++};
+		}
+
+		constexpr iterator& operator--() {
+			--m_it1;
+			--m_it2;
+			return *this;
+		}
+
+		constexpr iterator operator--(int) {
+			return iterator{m_it1--, m_it2--};
+		}
+
+	private:
+		Iterator1 m_it1;
+		Iterator2 m_it2;
+	};
+
+	constexpr iterator begin() const {
+		return iterator{std::begin(m_range1), std::begin(m_range2)};
+	}
+
+	constexpr sentinel end() const {
+		return sentinel{std::end(m_range1), std::end(m_range2)};
+	}
+
+	Range1 m_range1;
+	Range2 m_range2;
+};
+
+} // namespace detail
+
+template <typename First, typename Last, typename T = std::remove_cv_t<std::remove_reference_t<First>>>
+[[nodiscard]] constexpr auto range(First&& first, Last&& last) {
+	return detail::range_view<T>{std::forward<First>(first), std::forward<Last>(last)};
+}
+
+template <typename Last, typename T = std::remove_cv_t<std::remove_reference_t<Last>>>
+[[nodiscard]] constexpr auto range(Last&& last) {
+	return detail::range_view<T>{T{}, std::forward<Last>(last)};
+}
+
+template <typename Range, typename Iterator = decltype(std::begin(std::declval<Range>())),
+	typename Sentinel = decltype(std::end(std::declval<Range>()))>
+[[nodiscard]] constexpr auto enumerate(Range&& range) {
+	return detail::enumerate_view<Range, Iterator, Sentinel>{std::forward<Range>(range)};
+}
+
+template <typename Range1, typename Range2, typename Iterator1 = decltype(std::begin(std::declval<Range1>())),
+	typename Iterator2 = decltype(std::begin(std::declval<Range2>())), typename Sentinel1 = decltype(std::end(std::declval<Range1>())),
+	typename Sentinel2 = decltype(std::end(std::declval<Range2>()))>
+[[nodiscard]] constexpr auto zip(Range1&& range1, Range2&& range2) {
+	return detail::zip_view<Range1, Range2, Iterator1, Iterator2, Sentinel1, Sentinel2>{
+		std::forward<Range1>(range1), std::forward<Range2>(range2)};
+}
+
+} // namespace asic
+
+#endif // ASIC_ALGORITHM_H
\ No newline at end of file
diff --git a/src/debug.h b/src/debug.h
new file mode 100644
index 0000000000000000000000000000000000000000..a11aa057db644dbe2d29399398a1f48ca599876f
--- /dev/null
+++ b/src/debug.h
@@ -0,0 +1,80 @@
+#ifndef ASIC_DEBUG_H
+#define ASIC_DEBUG_H
+
+#ifndef NDEBUG
+#define ASIC_ENABLE_DEBUG_LOGGING 1
+#define ASIC_ENABLE_ASSERTS 1
+#else
+#define ASIC_ENABLE_DEBUG_LOGGING 0
+#define ASIC_ENABLE_ASSERTS 0
+#endif // NDEBUG
+
+#if ASIC_ENABLE_DEBUG_LOGGING
+#include <filesystem>
+#include <fmt/format.h>
+#include <fstream>
+#include <ostream>
+#include <string_view>
+#include <utility>
+#endif // ASIC_ENABLE_DEBUG_LOGGING
+
+#if ASIC_ENABLE_ASSERTS
+#include <filesystem>
+#include <cstdlib>
+#include <cstdio>
+#include <string_view>
+#include <fmt/format.h>
+#endif // ASIC_ENABLE_ASSERTS
+
+namespace asic {
+
+constexpr auto debug_log_filename = "_b_asic_debug_log.txt";
+
+namespace detail {
+
+#if ASIC_ENABLE_DEBUG_LOGGING
+inline void log_debug_msg_string(std::string_view file, int line, std::string_view string) {
+	static auto log_file = std::ofstream{debug_log_filename, std::ios::trunc};
+	log_file << fmt::format("{:<40}: {}", fmt::format("{}:{}", std::filesystem::path{file}.filename().generic_string(), line), string)
+			 << std::endl;
+}
+
+template <typename Format, typename... Args>
+inline void log_debug_msg(std::string_view file, int line, Format&& format, Args&&... args) {
+	log_debug_msg_string(file, line, fmt::format(std::forward<Format>(format), std::forward<Args>(args)...));
+}
+#endif // ASIC_ENABLE_DEBUG_LOGGING
+
+#if ASIC_ENABLE_ASSERTS
+inline void fail_assert(std::string_view file, int line, std::string_view condition_string) {
+#if ASIC_ENABLE_DEBUG_LOGGING
+	log_debug_msg(file, line, "Assertion failed: {}", condition_string);
+#endif // ASIC_ENABLE_DEBUG_LOGGING
+	fmt::print(stderr, "{}:{}: Assertion failed: {}\n", std::filesystem::path{file}.filename().generic_string(), line, condition_string);
+	std::abort();
+}
+
+template <typename BoolConvertible>
+inline void check_assert(std::string_view file, int line, std::string_view condition_string, BoolConvertible&& condition) {
+	if (!static_cast<bool>(condition)) {
+		fail_assert(file, line, condition_string);
+	}
+}
+#endif // ASIC_ENABLE_ASSERTS
+
+} // namespace detail
+} // namespace asic
+
+#if ASIC_ENABLE_DEBUG_LOGGING
+#define ASIC_DEBUG_MSG(...) (asic::detail::log_debug_msg(__FILE__, __LINE__, __VA_ARGS__))
+#else
+#define ASIC_DEBUG_MSG(...) ((void)0)
+#endif // ASIC_ENABLE_DEBUG_LOGGING
+
+#if ASIC_ENABLE_ASSERTS
+#define ASIC_ASSERT(condition) (asic::detail::check_assert(__FILE__, __LINE__, #condition, (condition)))
+#else
+#define ASIC_ASSERT(condition) ((void)0)
+#endif
+
+#endif // ASIC_DEBUG_H
\ No newline at end of file
diff --git a/src/main.cpp b/src/main.cpp
index 75a77ef58b86cd29238205a078cec780a6ba9a36..f5c4be532aa47468592e2e2f008308d1724e41b8 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,21 +1,9 @@
-#include <pybind11/pybind11.h>
-
-namespace py = pybind11;
-
-namespace asic {
-
-int add(int a, int b) {
-	return a + b;
-}
-
-int sub(int a, int b) {
-	return a - b;
-}
-
-} // namespace asic
-
-PYBIND11_MODULE(_b_asic, m) {
-	m.doc() = "Better ASIC Toolbox Extension Module.";
-	m.def("add", &asic::add, "A function which adds two numbers.", py::arg("a"), py::arg("b"));
-	m.def("sub", &asic::sub, "A function which subtracts two numbers.", py::arg("a"), py::arg("b"));
+#include "simulation.h"
+#include <pybind11/pybind11.h>
+
+namespace py = pybind11;
+
+PYBIND11_MODULE(_b_asic, module) {
+	module.doc() = "Better ASIC Toolbox Extension Module.";
+	asic::define_simulation_class(module);
 }
\ No newline at end of file
diff --git a/src/number.h b/src/number.h
new file mode 100644
index 0000000000000000000000000000000000000000..9cb5b42f53be4eb0cfcc86d00be65005147384e2
--- /dev/null
+++ b/src/number.h
@@ -0,0 +1,13 @@
+#ifndef ASIC_NUMBER_H
+#define ASIC_NUMBER_H
+
+#include <complex>
+#include <pybind11/complex.h>
+
+namespace asic {
+
+using number = std::complex<double>;
+
+} // namespace asic
+
+#endif // ASIC_NUMBER_H
\ No newline at end of file
diff --git a/src/simulation.cpp b/src/simulation.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..33280f604be77614f7eadf1ad40d868177dd6e95
--- /dev/null
+++ b/src/simulation.cpp
@@ -0,0 +1,61 @@
+#include "simulation.h"
+#include "simulation/simulation.h"
+
+namespace py = pybind11;
+
+namespace asic {
+
+void define_simulation_class(pybind11::module& module) {
+	// clang-format off
+	py::class_<simulation>(module, "FastSimulation")
+		.def(py::init<py::handle>(),
+			py::arg("sfg"),
+			"SFG Constructor.")
+
+		.def(py::init<py::handle, std::optional<std::vector<std::optional<input_provider_t>>>>(),
+			py::arg("sfg"), py::arg("input_providers"),
+			"SFG Constructor.")
+
+		.def("set_input", &simulation::set_input,
+			py::arg("index"), py::arg("input_provider"),
+			"Set the input function used to get values for the specific input at the given index to the internal SFG.")
+
+		.def("set_inputs", &simulation::set_inputs,
+			py::arg("input_providers"),
+			"Set the input functions used to get values for the inputs to the internal SFG.")
+
+		.def("step", &simulation::step,
+			py::arg("save_results") = true, py::arg("bits_override") = py::none{}, py::arg("truncate") = true,
+			"Run one iteration of the simulation and return the resulting output values.")
+
+		.def("run_until", &simulation::run_until,
+			py::arg("iteration"), py::arg("save_results") = true, py::arg("bits_override") = py::none{}, py::arg("truncate") = true,
+			"Run the simulation until its iteration is greater than or equal to the given iteration\n"
+			"and return the output values of the last iteration.")
+
+		.def("run_for", &simulation::run_for,
+			py::arg("iterations"), py::arg("save_results") = true, py::arg("bits_override") = py::none{}, py::arg("truncate") = true,
+			"Run a given number of iterations of the simulation and return the output values of the last iteration.")
+
+		.def("run", &simulation::run,
+			py::arg("save_results") = true, py::arg("bits_override") = py::none{}, py::arg("truncate") = true,
+			"Run the simulation until the end of its input arrays and return the output values of the last iteration.")
+
+		.def_property_readonly("iteration", &simulation::iteration,
+			"Get the current iteration number of the simulation.")
+
+		.def_property_readonly("results", &simulation::results,
+			"Get a mapping from result keys to numpy arrays containing all results, including intermediate values,\n"
+			"calculated for each iteration up until now that was run with save_results enabled.\n"
+			"The mapping is indexed using the key() method of Operation with the appropriate output index.\n"
+			"Example result after 3 iterations: {\"c1\": [3, 6, 7], \"c2\": [4, 5, 5], \"bfly1.0\": [7, 0, 0], \"bfly1.1\": [-1, 0, 2], \"0\": [7, -2, -1]}")
+
+		.def("clear_results", &simulation::clear_results,
+			"Clear all results that were saved until now.")
+
+		.def("clear_state", &simulation::clear_state,
+			"Clear all current state of the simulation, except for the results and iteration.");
+	// clang-format on
+}
+
+} // namespace asic
\ No newline at end of file
diff --git a/src/simulation.h b/src/simulation.h
new file mode 100644
index 0000000000000000000000000000000000000000..aefa3a4e92b861b9c7b795c7301f900dc54ace6f
--- /dev/null
+++ b/src/simulation.h
@@ -0,0 +1,12 @@
+#ifndef ASIC_SIMULATION_H
+#define ASIC_SIMULATION_H
+
+#include <pybind11/pybind11.h>
+
+namespace asic {
+
+void define_simulation_class(pybind11::module& module);
+
+} // namespace asic
+
+#endif // ASIC_SIMULATION_H
\ No newline at end of file
diff --git a/src/simulation/compile.cpp b/src/simulation/compile.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7fa7ac6721c9af0f5b9decdf8c3f0dca653e47ec
--- /dev/null
+++ b/src/simulation/compile.cpp
@@ -0,0 +1,313 @@
+#define NOMINMAX
+#include "compile.h"
+
+#include "../algorithm.h"
+#include "../debug.h"
+#include "../span.h"
+#include "format_code.h"
+
+#include <Python.h>
+#include <fmt/format.h>
+#include <limits>
+#include <optional>
+#include <string_view>
+#include <tuple>
+#include <unordered_map>
+#include <utility>
+
+namespace py = pybind11;
+
+namespace asic {
+
+[[nodiscard]] static result_key key_base(py::handle op, std::string_view prefix) {
+	auto const graph_id = op.attr("graph_id").cast<std::string_view>();
+	return (prefix.empty()) ? result_key{graph_id} : fmt::format("{}.{}", prefix, graph_id);
+}
+
+[[nodiscard]] static result_key key_of_output(py::handle op, std::size_t output_index, std::string_view prefix) {
+	auto const base = key_base(op, prefix);
+	if (base.empty()) {
+		return fmt::to_string(output_index);
+	}
+	if (op.attr("output_count").cast<std::size_t>() == 1) {
+		return base;
+	}
+	return fmt::format("{}.{}", base, output_index);
+}
+
+class compiler final {
+public:
+	simulation_code compile(py::handle sfg) {
+		ASIC_DEBUG_MSG("Compiling code...");
+		this->initialize_code(sfg.attr("input_count").cast<std::size_t>(), sfg.attr("output_count").cast<std::size_t>());
+		auto deferred_delays = delay_queue{};
+		this->add_outputs(sfg, deferred_delays);
+		this->add_deferred_delays(std::move(deferred_delays));
+		this->resolve_invalid_result_indices();
+		ASIC_DEBUG_MSG("Compiled code:\n{}\n", format_compiled_simulation_code(m_code));
+		return std::move(m_code);
+	}
+
+private:
+	struct sfg_info final {
+		py::handle sfg;
+		std::size_t prefix_length;
+
+		sfg_info(py::handle sfg, std::size_t prefix_length)
+			: sfg(sfg)
+			, prefix_length(prefix_length) {}
+
+		[[nodiscard]] std::size_t find_input_operation_index(py::handle op) const {
+			for (auto const& [i, in] : enumerate(sfg.attr("input_operations"))) {
+				if (in.is(op)) {
+					return i;
+				}
+			}
+			throw py::value_error{"Stray Input operation in simulation SFG"};
+		}
+	};
+
+	using sfg_info_stack = std::vector<sfg_info>;
+	using delay_queue = std::vector<std::tuple<std::size_t, py::handle, std::string, sfg_info_stack>>;
+	using added_output_cache = std::unordered_set<PyObject const*>;
+	using added_result_cache = std::unordered_map<PyObject const*, result_index_t>;
+	using added_custom_operation_cache = std::unordered_map<PyObject const*, std::size_t>;
+
+	static constexpr auto no_result_index = std::numeric_limits<result_index_t>::max();
+
+	void initialize_code(std::size_t input_count, std::size_t output_count) {
+		m_code.required_stack_size = 0;
+		m_code.input_count = input_count;
+		m_code.output_count = output_count;
+	}
+
+	void add_outputs(py::handle sfg, delay_queue& deferred_delays) {
+		for (auto const i : range(m_code.output_count)) {
+			this->add_operation_output(sfg, i, std::string_view{}, sfg_info_stack{}, deferred_delays);
+		}
+	}
+
+	void add_deferred_delays(delay_queue&& deferred_delays) {
+		while (!deferred_delays.empty()) {
+			auto new_deferred_delays = delay_queue{};
+			for (auto const& [delay_index, op, prefix, sfg_stack] : deferred_delays) {
+				this->add_source(op, 0, prefix, sfg_stack, deferred_delays);
+				this->add_instruction(instruction_type::update_delay, no_result_index, -1).index = delay_index;
+			}
+			deferred_delays = new_deferred_delays;
+		}
+	}
+
+	void resolve_invalid_result_indices() {
+		for (auto& instruction : m_code.instructions) {
+			if (instruction.result_index == no_result_index) {
+				instruction.result_index = static_cast<result_index_t>(m_code.result_keys.size());
+			}
+		}
+	}
+
+	[[nodiscard]] static sfg_info_stack push_sfg(sfg_info_stack const& sfg_stack, py::handle sfg, std::size_t prefix_length) {
+		auto const new_size = static_cast<std::size_t>(sfg_stack.size() + 1);
+		auto new_sfg_stack = sfg_info_stack{};
+		new_sfg_stack.reserve(new_size);
+		for (auto const& info : sfg_stack) {
+			new_sfg_stack.push_back(info);
+		}
+		new_sfg_stack.emplace_back(sfg, prefix_length);
+		return new_sfg_stack;
+	}
+
+	[[nodiscard]] static sfg_info_stack pop_sfg(sfg_info_stack const& sfg_stack) {
+		ASIC_ASSERT(!sfg_stack.empty());
+		auto const new_size = static_cast<std::size_t>(sfg_stack.size() - 1);
+		auto new_sfg_stack = sfg_info_stack{};
+		new_sfg_stack.reserve(new_size);
+		for (auto const& info : span{sfg_stack}.first(new_size)) {
+			new_sfg_stack.push_back(info);
+		}
+		return new_sfg_stack;
+	}
+
+	instruction& add_instruction(instruction_type type, result_index_t result_index, std::ptrdiff_t stack_diff) {
+		m_stack_depth += stack_diff;
+		if (m_stack_depth < 0) {
+			throw py::value_error{"Detected input/output count mismatch in simulation SFG"};
+		}
+		if (auto const stack_size = static_cast<std::size_t>(m_stack_depth); stack_size > m_code.required_stack_size) {
+			m_code.required_stack_size = stack_size;
+		}
+		auto& instruction = m_code.instructions.emplace_back();
+		instruction.type = type;
+		instruction.result_index = result_index;
+		return instruction;
+	}
+
+	[[nodiscard]] std::optional<result_index_t> begin_operation_output(py::handle op, std::size_t output_index, std::string_view prefix) {
+		auto const pointer = op.attr("outputs")[py::int_{output_index}].ptr();
+		if (m_incomplete_outputs.count(pointer) != 0) {
+			// Make sure the output doesn't depend on its own value, unless it's a delay operation.
+			if (op.attr("type_name")().cast<std::string_view>() != "t") {
+				throw py::value_error{"Direct feedback loop detected in simulation SFG"};
+			}
+		}
+		// Try to add a new result.
+		auto const [it, inserted] = m_added_results.try_emplace(pointer, static_cast<result_index_t>(m_code.result_keys.size()));
+		if (inserted) {
+			if (m_code.result_keys.size() >= static_cast<std::size_t>(std::numeric_limits<result_index_t>::max())) {
+				throw py::value_error{fmt::format("Simulation SFG requires too many outputs to be stored (limit: {})",
+												  std::numeric_limits<result_index_t>::max())};
+			}
+			m_code.result_keys.push_back(key_of_output(op, output_index, prefix));
+			m_incomplete_outputs.insert(pointer);
+			return it->second;
+		}
+		// If the result has already been added, we re-use the old result and
+		// return std::nullopt to indicate that we don't need to add all the required instructions again.
+		this->add_instruction(instruction_type::push_result, it->second, 1).index = static_cast<std::size_t>(it->second);
+		return std::nullopt;
+	}
+
+	void end_operation_output(py::handle op, std::size_t output_index) {
+		auto const pointer = op.attr("outputs")[py::int_{output_index}].ptr();
+		[[maybe_unused]] auto const erased = m_incomplete_outputs.erase(pointer);
+		ASIC_ASSERT(erased == 1);
+	}
+
+	[[nodiscard]] std::size_t try_add_custom_operation(py::handle op) {
+		auto const [it, inserted] = m_added_custom_operations.try_emplace(op.ptr(), m_added_custom_operations.size());
+		if (inserted) {
+			auto& custom_operation = m_code.custom_operations.emplace_back();
+			custom_operation.evaluate_output = op.attr("evaluate_output");
+			custom_operation.input_count = op.attr("input_count").cast<std::size_t>();
+			custom_operation.output_count = op.attr("output_count").cast<std::size_t>();
+		}
+		return it->second;
+	}
+
+	[[nodiscard]] std::size_t add_delay_info(number initial_value, result_index_t result_index) {
+		auto const delay_index = m_code.delays.size();
+		auto& delay = m_code.delays.emplace_back();
+		delay.initial_value = initial_value;
+		delay.result_index = result_index;
+		return delay_index;
+	}
+
+	void add_source(py::handle op, std::size_t input_index, std::string_view prefix, sfg_info_stack const& sfg_stack,
+					delay_queue& deferred_delays) {
+		auto const signal = py::object{op.attr("inputs")[py::int_{input_index}].attr("signals")[py::int_{0}]};
+		auto const src = py::handle{signal.attr("source")};
+		auto const operation = py::handle{src.attr("operation")};
+		auto const index = src.attr("index").cast<std::size_t>();
+		this->add_operation_output(operation, index, prefix, sfg_stack, deferred_delays);
+		if (!signal.attr("bits").is_none()) {
+			auto const bits = signal.attr("bits").cast<std::size_t>();
+			if (bits > 64) {
+				throw py::value_error{"Cannot truncate to more than 64 bits"};
+			}
+			this->add_instruction(instruction_type::truncate, no_result_index, 0).bit_mask = static_cast<std::int64_t>(
+				(std::int64_t{1} << bits) - 1);
+		}
+	}
+
+	void add_unary_operation_output(py::handle op, result_index_t result_index, std::string_view prefix, sfg_info_stack const& sfg_stack,
+									delay_queue& deferred_delays, instruction_type type) {
+		this->add_source(op, 0, prefix, sfg_stack, deferred_delays);
+		this->add_instruction(type, result_index, 0);
+	}
+
+	void add_binary_operation_output(py::handle op, result_index_t result_index, std::string_view prefix, sfg_info_stack const& sfg_stack,
+									 delay_queue& deferred_delays, instruction_type type) {
+		this->add_source(op, 0, prefix, sfg_stack, deferred_delays);
+		this->add_source(op, 1, prefix, sfg_stack, deferred_delays);
+		this->add_instruction(type, result_index, -1);
+	}
+
+	void add_operation_output(py::handle op, std::size_t output_index, std::string_view prefix, sfg_info_stack const& sfg_stack,
+							  delay_queue& deferred_delays) {
+		auto const type_name = op.attr("type_name")().cast<std::string_view>();
+		if (type_name == "out") {
+			this->add_source(op, 0, prefix, sfg_stack, deferred_delays);
+		} else if (auto const result_index = this->begin_operation_output(op, output_index, prefix)) {
+			if (type_name == "c") {
+				this->add_instruction(instruction_type::push_constant, *result_index, 1).value = op.attr("value").cast<number>();
+			} else if (type_name == "add") {
+				this->add_binary_operation_output(op, *result_index, prefix, sfg_stack, deferred_delays, instruction_type::addition);
+			} else if (type_name == "sub") {
+				this->add_binary_operation_output(op, *result_index, prefix, sfg_stack, deferred_delays, instruction_type::subtraction);
+			} else if (type_name == "mul") {
+				this->add_binary_operation_output(op, *result_index, prefix, sfg_stack, deferred_delays, instruction_type::multiplication);
+			} else if (type_name == "div") {
+				this->add_binary_operation_output(op, *result_index, prefix, sfg_stack, deferred_delays, instruction_type::division);
+			} else if (type_name == "min") {
+				this->add_binary_operation_output(op, *result_index, prefix, sfg_stack, deferred_delays, instruction_type::min);
+			} else if (type_name == "max") {
+				this->add_binary_operation_output(op, *result_index, prefix, sfg_stack, deferred_delays, instruction_type::max);
+			} else if (type_name == "sqrt") {
+				this->add_unary_operation_output(op, *result_index, prefix, sfg_stack, deferred_delays, instruction_type::square_root);
+			} else if (type_name == "conj") {
+				this->add_unary_operation_output(
+					op, *result_index, prefix, sfg_stack, deferred_delays, instruction_type::complex_conjugate);
+			} else if (type_name == "abs") {
+				this->add_unary_operation_output(op, *result_index, prefix, sfg_stack, deferred_delays, instruction_type::absolute);
+			} else if (type_name == "cmul") {
+				this->add_source(op, 0, prefix, sfg_stack, deferred_delays);
+				this->add_instruction(instruction_type::constant_multiplication, *result_index, 0).value = op.attr("value").cast<number>();
+			} else if (type_name == "bfly") {
+				if (output_index == 0) {
+					this->add_source(op, 0, prefix, sfg_stack, deferred_delays);
+					this->add_source(op, 1, prefix, sfg_stack, deferred_delays);
+					this->add_instruction(instruction_type::addition, *result_index, -1);
+				} else {
+					this->add_source(op, 0, prefix, sfg_stack, deferred_delays);
+					this->add_source(op, 1, prefix, sfg_stack, deferred_delays);
+					this->add_instruction(instruction_type::subtraction, *result_index, -1);
+				}
+			} else if (type_name == "in") {
+				if (sfg_stack.empty()) {
+					throw py::value_error{"Encountered Input operation outside SFG in simulation"};
+				}
+				auto const& info = sfg_stack.back();
+				auto const input_index = info.find_input_operation_index(op);
+				if (sfg_stack.size() == 1) {
+					this->add_instruction(instruction_type::push_input, *result_index, 1).index = input_index;
+				} else {
+					this->add_source(info.sfg, input_index, prefix.substr(0, info.prefix_length), pop_sfg(sfg_stack), deferred_delays);
+					this->add_instruction(instruction_type::forward_value, *result_index, 0);
+				}
+			} else if (type_name == "t") {
+				auto const delay_index = this->add_delay_info(op.attr("initial_value").cast<number>(), *result_index);
+				deferred_delays.emplace_back(delay_index, op, std::string{prefix}, sfg_stack);
+				this->add_instruction(instruction_type::push_delay, *result_index, 1).index = delay_index;
+			} else if (type_name == "sfg") {
+				auto const output_op = py::handle{op.attr("output_operations")[py::int_{output_index}]};
+				this->add_source(output_op, 0, key_base(op, prefix), push_sfg(sfg_stack, op, prefix.size()), deferred_delays);
+				this->add_instruction(instruction_type::forward_value, *result_index, 0);
+			} else {
+				auto const custom_operation_index = this->try_add_custom_operation(op);
+				auto const& custom_operation = m_code.custom_operations[custom_operation_index];
+				for (auto const i : range(custom_operation.input_count)) {
+					this->add_source(op, i, prefix, sfg_stack, deferred_delays);
+				}
+				auto const custom_source_index = m_code.custom_sources.size();
+				auto& custom_source = m_code.custom_sources.emplace_back();
+				custom_source.custom_operation_index = custom_operation_index;
+				custom_source.output_index = output_index;
+				auto const stack_diff = std::ptrdiff_t{1} - static_cast<std::ptrdiff_t>(custom_operation.input_count);
+				this->add_instruction(instruction_type::custom, *result_index, stack_diff).index = custom_source_index;
+			}
+			this->end_operation_output(op, output_index);
+		}
+	}
+
+	simulation_code m_code;
+	added_output_cache m_incomplete_outputs;
+	added_result_cache m_added_results;
+	added_custom_operation_cache m_added_custom_operations;
+	std::ptrdiff_t m_stack_depth = 0;
+};
+
+simulation_code compile_simulation(pybind11::handle sfg) {
+	return compiler{}.compile(sfg);
+}
+
+} // namespace asic
\ No newline at end of file
diff --git a/src/simulation/compile.h b/src/simulation/compile.h
new file mode 100644
index 0000000000000000000000000000000000000000..883f4c5832978ea1bfd33c767fc947c1efde718e
--- /dev/null
+++ b/src/simulation/compile.h
@@ -0,0 +1,61 @@
+#ifndef ASIC_SIMULATION_COMPILE_H
+#define ASIC_SIMULATION_COMPILE_H
+
+#include "instruction.h"
+
+#include <cstddef>
+#include <pybind11/pybind11.h>
+#include <string>
+#include <vector>
+
+namespace asic {
+
+using result_key = std::string;
+
+struct simulation_code final {
+	struct custom_operation final {
+		// Python function used to evaluate the custom operation.
+		pybind11::object evaluate_output;
+		// Number of inputs that the custom operation takes.
+		std::size_t input_count;
+		// Number of outputs that the custom operation gives.
+		std::size_t output_count;
+	};
+
+	struct custom_source final {
+		// Index into custom_operations where the custom_operation corresponding to this custom_source is located.
+		std::size_t custom_operation_index;
+		// Output index of the custom_operation that this source gets it value from.
+		std::size_t output_index;
+	};
+
+	struct delay_info final {
+		// Initial value to set at the start of the simulation.
+		number initial_value;
+		// The result index where the current value should be stored at the start of each iteration.
+		result_index_t result_index;
+	};
+
+	// Instructions to execute for one full iteration of the simulation.
+	std::vector<instruction> instructions;
+	// Custom operations used by the simulation.
+	std::vector<custom_operation> custom_operations;
+	// Signal sources that use custom operations.
+	std::vector<custom_source> custom_sources;
+	// Info about the delay operations used in the simulation.
+	std::vector<delay_info> delays;
+	// Keys for each result produced by the simulation. The index of the key matches the index of the result in the simulation state.
+	std::vector<result_key> result_keys;
+	// Number of values expected as input to the simulation.
+	std::size_t input_count;
+	// Number of values given as output from the simulation. This will be the number of values left on the stack after a full iteration of the simulation has been run.
+	std::size_t output_count;
+	// Maximum number of values that need to be able to fit on the stack in order to run a full iteration of the simulation.
+	std::size_t required_stack_size;
+};
+
+[[nodiscard]] simulation_code compile_simulation(pybind11::handle sfg);
+
+} // namespace asic
+
+#endif // ASIC_SIMULATION_COMPILE_H
\ No newline at end of file
diff --git a/src/simulation/format_code.h b/src/simulation/format_code.h
new file mode 100644
index 0000000000000000000000000000000000000000..5ebbb95d1f11eb18b915dbab9fbccbb82d83304c
--- /dev/null
+++ b/src/simulation/format_code.h
@@ -0,0 +1,129 @@
+#ifndef ASIC_SIMULATION_FORMAT_CODE_H
+#define ASIC_SIMULATION_FORMAT_CODE_H
+
+#include "../algorithm.h"
+#include "../debug.h"
+#include "../number.h"
+#include "compile.h"
+#include "instruction.h"
+
+#include <fmt/format.h>
+#include <string>
+
+namespace asic {
+
+[[nodiscard]] inline std::string format_number(number const& value) {
+	if (value.imag() == 0) {
+		return fmt::to_string(value.real());
+	}
+	if (value.real() == 0) {
+		return fmt::format("{}j", value.imag());
+	}
+	if (value.imag() < 0) {
+		return fmt::format("{}-{}j", value.real(), -value.imag());
+	}
+	return fmt::format("{}+{}j", value.real(), value.imag());
+}
+
+[[nodiscard]] inline std::string format_compiled_simulation_code_result_keys(simulation_code const& code) {
+	auto result = std::string{};
+	for (auto const& [i, result_key] : enumerate(code.result_keys)) {
+		result += fmt::format("{:>2}: \"{}\"\n", i, result_key);
+	}
+	return result;
+}
+
+[[nodiscard]] inline std::string format_compiled_simulation_code_delays(simulation_code const& code) {
+	auto result = std::string{};
+	for (auto const& [i, delay] : enumerate(code.delays)) {
+		ASIC_ASSERT(delay.result_index < code.result_keys.size());
+		result += fmt::format("{:>2}: Initial value: {}, Result: {}: \"{}\"\n",
+							  i,
+							  format_number(delay.initial_value),
+							  delay.result_index,
+							  code.result_keys[delay.result_index]);
+	}
+	return result;
+}
+
+[[nodiscard]] inline std::string format_compiled_simulation_code_instruction(instruction const& instruction) {
+	switch (instruction.type) {
+		// clang-format off
+		case instruction_type::push_input:              return fmt::format("push_input inputs[{}]", instruction.index);
+		case instruction_type::push_result:             return fmt::format("push_result results[{}]", instruction.index);
+		case instruction_type::push_delay:              return fmt::format("push_delay delays[{}]", instruction.index);
+		case instruction_type::push_constant:           return fmt::format("push_constant {}", format_number(instruction.value));
+		case instruction_type::truncate:                return fmt::format("truncate {:#018x}", instruction.bit_mask);
+		case instruction_type::addition:                return "addition";
+		case instruction_type::subtraction:             return "subtraction";
+		case instruction_type::multiplication:          return "multiplication";
+		case instruction_type::division:                return "division";
+		case instruction_type::min:                     return "min";
+		case instruction_type::max:                     return "max";
+		case instruction_type::square_root:             return "square_root";
+		case instruction_type::complex_conjugate:       return "complex_conjugate";
+		case instruction_type::absolute:                return "absolute";
+		case instruction_type::constant_multiplication: return fmt::format("constant_multiplication {}", format_number(instruction.value));
+		case instruction_type::update_delay:            return fmt::format("update_delay delays[{}]", instruction.index);
+		case instruction_type::custom:                  return fmt::format("custom custom_sources[{}]", instruction.index);
+		case instruction_type::forward_value:           return "forward_value";
+		// clang-format on
+	}
+	return std::string{};
+}
+
+[[nodiscard]] inline std::string format_compiled_simulation_code_instructions(simulation_code const& code) {
+	auto result = std::string{};
+	for (auto const& [i, instruction] : enumerate(code.instructions)) {
+		auto instruction_string = format_compiled_simulation_code_instruction(instruction);
+		if (instruction.result_index < code.result_keys.size()) {
+			instruction_string = fmt::format(
+				"{:<26} -> {}: \"{}\"", instruction_string, instruction.result_index, code.result_keys[instruction.result_index]);
+		}
+		result += fmt::format("{:>2}: {}\n", i, instruction_string);
+	}
+	return result;
+}
+
+[[nodiscard]] inline std::string format_compiled_simulation_code(simulation_code const& code) {
+	return fmt::format(
+		"==============================================\n"
+		"> Code stats\n"
+		"==============================================\n"
+		"Input count: {}\n"
+		"Output count: {}\n"
+		"Instruction count: {}\n"
+		"Required stack size: {}\n"
+		"Delay count: {}\n"
+		"Result count: {}\n"
+		"Custom operation count: {}\n"
+		"Custom source count: {}\n"
+		"==============================================\n"
+		"> Delays\n"
+		"==============================================\n"
+		"{}"
+		"==============================================\n"
+		"> Result keys\n"
+		"==============================================\n"
+		"{}"
+		"==============================================\n"
+		"> Instructions\n"
+		"==============================================\n"
+		"{}"
+		"==============================================",
+		code.input_count,
+		code.output_count,
+		code.instructions.size(),
+		code.required_stack_size,
+		code.delays.size(),
+		code.result_keys.size(),
+		code.custom_operations.size(),
+		code.custom_sources.size(),
+		format_compiled_simulation_code_delays(code),
+		format_compiled_simulation_code_result_keys(code),
+		format_compiled_simulation_code_instructions(code));
+}
+
+} // namespace asic
+
+#endif // ASIC_SIMULATION_FORMAT_CODE
\ No newline at end of file
diff --git a/src/simulation/instruction.h b/src/simulation/instruction.h
new file mode 100644
index 0000000000000000000000000000000000000000..d650c651394a243c52eee7e5ad2fe463f96bdad7
--- /dev/null
+++ b/src/simulation/instruction.h
@@ -0,0 +1,57 @@
+#ifndef ASIC_SIMULATION_INSTRUCTION_H
+#define ASIC_SIMULATION_INSTRUCTION_H
+
+#include "../number.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+
+namespace asic {
+
+enum class instruction_type : std::uint8_t {
+	push_input,              // push(inputs[index])
+	push_result,             // push(results[index])
+	push_delay,              // push(delays[index])
+	push_constant,           // push(value)
+	truncate,                // push(trunc(pop(), bit_mask))
+	addition,                // push(pop() + pop())
+	subtraction,             // push(pop() - pop())
+	multiplication,          // push(pop() * pop())
+	division,                // push(pop() / pop())
+	min,                     // push(min(pop(), pop()))
+	max,                     // push(max(pop(), pop()))
+	square_root,             // push(sqrt(pop()))
+	complex_conjugate,       // push(conj(pop()))
+	absolute,                // push(abs(pop()))
+	constant_multiplication, // push(pop() * value)
+	update_delay,            // delays[index] = pop()
+	custom,                  // Custom operation. Uses custom_source[index].
+	forward_value            // Forward the current value on the stack (push(pop()), i.e. do nothing).
+};
+
+using result_index_t = std::uint16_t;
+
+struct instruction final {
+	constexpr instruction() noexcept
+		: index(0)
+		, result_index(0)
+		, type(instruction_type::forward_value) {}
+
+	union {
+		// Index used by push_input, push_result, delay and custom.
+		std::size_t index;
+		// Bit mask used by truncate.
+		std::int64_t bit_mask;
+		// Constant value used by push_constant and constant_multiplication.
+		number value;
+	};
+	// Index into where the result of the instruction will be stored. If the result should be ignored, this index will be one past the last valid result index.
+	result_index_t result_index;
+	// Specifies what kind of operation the instruction should execute.
+	instruction_type type;
+};
+
+} // namespace asic
+
+#endif // ASIC_SIMULATION_INSTRUCTION_H
\ No newline at end of file
diff --git a/src/simulation/run.cpp b/src/simulation/run.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c14fa192bf0fd52a6b661aa4fb392b78859c382f
--- /dev/null
+++ b/src/simulation/run.cpp
@@ -0,0 +1,176 @@
+#define NOMINMAX
+#include "run.h"
+
+#include "../algorithm.h"
+#include "../debug.h"
+#include "format_code.h"
+
+#include <algorithm>
+#include <complex>
+#include <cstddef>
+#include <fmt/format.h>
+#include <iterator>
+#include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+#include <stdexcept>
+
+namespace py = pybind11;
+
+namespace asic {
+
+[[nodiscard]] static number truncate_value(number value, std::int64_t bit_mask) {
+	if (value.imag() != 0) {
+		throw py::type_error{"Complex value cannot be truncated"};
+	}
+	return number{static_cast<number::value_type>(static_cast<std::int64_t>(value.real()) & bit_mask)};
+}
+
+[[nodiscard]] static std::int64_t setup_truncation_parameters(bool& truncate, std::optional<std::uint8_t>& bits_override) {
+	if (truncate && bits_override) {
+		truncate = false; // Ignore truncate instructions, they will be truncated using bits_override instead.
+		if (*bits_override > 64) {
+			throw py::value_error{"Cannot truncate to more than 64 bits"};
+		}
+		return static_cast<std::int64_t>((std::int64_t{1} << *bits_override) - 1); // Return the bit mask override to use.
+	}
+	bits_override.reset(); // Don't use bits_override if truncate is false.
+	return std::int64_t{};
+}
+
+simulation_state run_simulation(simulation_code const& code, span<number const> inputs, span<number> delays,
+								std::optional<std::uint8_t> bits_override, bool truncate) {
+	ASIC_ASSERT(inputs.size() == code.input_count);
+	ASIC_ASSERT(delays.size() == code.delays.size());
+	ASIC_ASSERT(code.output_count <= code.required_stack_size);
+
+	auto state = simulation_state{};
+
+	// Setup results.
+	state.results.resize(code.result_keys.size() + 1); // Add one space to store ignored results.
+	// Initialize delay results to their current values.
+	for (auto const& [i, delay] : enumerate(code.delays)) {
+		state.results[delay.result_index] = delays[i];
+	}
+
+	// Setup stack.
+	state.stack.resize(code.required_stack_size);
+	auto stack_pointer = state.stack.data();
+
+	// Utility functions to make the stack manipulation code below more readable.
+	// Should hopefully be inlined by the compiler.
+	auto const push = [&](number value) -> void {
+		ASIC_ASSERT(std::distance(state.stack.data(), stack_pointer) < static_cast<std::ptrdiff_t>(state.stack.size()));
+		*stack_pointer++ = value;
+	};
+	auto const pop = [&]() -> number {
+		ASIC_ASSERT(std::distance(state.stack.data(), stack_pointer) > std::ptrdiff_t{0});
+		return *--stack_pointer;
+	};
+	auto const peek = [&]() -> number {
+		ASIC_ASSERT(std::distance(state.stack.data(), stack_pointer) > std::ptrdiff_t{0});
+		ASIC_ASSERT(std::distance(state.stack.data(), stack_pointer) <= static_cast<std::ptrdiff_t>(state.stack.size()));
+		return *(stack_pointer - 1);
+	};
+
+	// Check if results should be truncated.
+	auto const bit_mask_override = setup_truncation_parameters(truncate, bits_override);
+
+	// Hot instruction evaluation loop.
+	for (auto const& instruction : code.instructions) {
+		ASIC_DEBUG_MSG("Evaluating {}.", format_compiled_simulation_code_instruction(instruction));
+		// Execute the instruction.
+		switch (instruction.type) {
+			case instruction_type::push_input:
+				push(inputs[instruction.index]);
+				break;
+			case instruction_type::push_result:
+				push(state.results[instruction.index]);
+				break;
+			case instruction_type::push_delay:
+				push(delays[instruction.index]);
+				break;
+			case instruction_type::push_constant:
+				push(instruction.value);
+				break;
+			case instruction_type::truncate:
+				if (truncate) {
+					push(truncate_value(pop(), instruction.bit_mask));
+				}
+				break;
+			case instruction_type::addition:
+				push(pop() + pop());
+				break;
+			case instruction_type::subtraction:
+				push(pop() - pop());
+				break;
+			case instruction_type::multiplication:
+				push(pop() * pop());
+				break;
+			case instruction_type::division:
+				push(pop() / pop());
+				break;
+			case instruction_type::min: {
+				auto const lhs = pop();
+				auto const rhs = pop();
+				if (lhs.imag() != 0 || rhs.imag() != 0) {
+					throw std::runtime_error{"Min does not support complex numbers."};
+				}
+				push(std::min(lhs.real(), rhs.real()));
+				break;
+			}
+			case instruction_type::max: {
+				auto const lhs = pop();
+				auto const rhs = pop();
+				if (lhs.imag() != 0 || rhs.imag() != 0) {
+					throw std::runtime_error{"Max does not support complex numbers."};
+				}
+				push(std::max(lhs.real(), rhs.real()));
+				break;
+			}
+			case instruction_type::square_root:
+				push(std::sqrt(pop()));
+				break;
+			case instruction_type::complex_conjugate:
+				push(std::conj(pop()));
+				break;
+			case instruction_type::absolute:
+				push(number{std::abs(pop())});
+				break;
+			case instruction_type::constant_multiplication:
+				push(pop() * instruction.value);
+				break;
+			case instruction_type::update_delay:
+				delays[instruction.index] = pop();
+				break;
+			case instruction_type::custom: {
+				using namespace pybind11::literals;
+				auto const& src = code.custom_sources[instruction.index];
+				auto const& op = code.custom_operations[src.custom_operation_index];
+				auto input_values = std::vector<number>{};
+				input_values.reserve(op.input_count);
+				for (auto i = std::size_t{0}; i < op.input_count; ++i) {
+					input_values.push_back(pop());
+				}
+				push(op.evaluate_output(src.output_index, std::move(input_values), "truncate"_a = truncate).cast<number>());
+				break;
+			}
+			case instruction_type::forward_value:
+				// Do nothing, since doing push(pop()) would be pointless.
+				break;
+		}
+		// If we've been given a global override for how many bits to use, always truncate the result.
+		if (bits_override) {
+			push(truncate_value(pop(), bit_mask_override));
+		}
+		// Store the result.
+		state.results[instruction.result_index] = peek();
+	}
+
+	// Remove the space that we used for ignored results.
+	state.results.pop_back();
+	// Erase the portion of the stack that does not contain the output values.
+	state.stack.erase(state.stack.begin() + static_cast<std::ptrdiff_t>(code.output_count), state.stack.end());
+	return state;
+}
+
+} // namespace asic
\ No newline at end of file
diff --git a/src/simulation/run.h b/src/simulation/run.h
new file mode 100644
index 0000000000000000000000000000000000000000..2174c571ef59f3e12236471e3e064f2619c38a60
--- /dev/null
+++ b/src/simulation/run.h
@@ -0,0 +1,23 @@
+#ifndef ASIC_SIMULATION_RUN_H
+#define ASIC_SIMULATION_RUN_H
+
+#include "../number.h"
+#include "../span.h"
+#include "compile.h"
+
+#include <cstdint>
+#include <vector>
+
+namespace asic {
+
+struct simulation_state final {
+	std::vector<number> stack;
+	std::vector<number> results;
+};
+
+simulation_state run_simulation(simulation_code const& code, span<number const> inputs, span<number> delays,
+								std::optional<std::uint8_t> bits_override, bool truncate);
+
+} // namespace asic
+
+#endif // ASIC_SIMULATION_RUN_H
\ No newline at end of file
diff --git a/src/simulation/simulation.cpp b/src/simulation/simulation.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3af24c10e62bfe09590a05153abd25468d6bee4c
--- /dev/null
+++ b/src/simulation/simulation.cpp
@@ -0,0 +1,129 @@
+#define NOMINMAX
+#include "simulation.h"
+
+#include "../algorithm.h"
+#include "../debug.h"
+#include "compile.h"
+#include "run.h"
+
+#include <fmt/format.h>
+#include <limits>
+#include <pybind11/numpy.h>
+#include <utility>
+
+namespace py = pybind11;
+
+namespace asic {
+
+simulation::simulation(pybind11::handle sfg, std::optional<std::vector<std::optional<input_provider_t>>> input_providers)
+	: m_code(compile_simulation(sfg))
+	, m_input_functions(sfg.attr("input_count").cast<std::size_t>(), [](iteration_t) -> number { return number{}; }) {
+	m_delays.reserve(m_code.delays.size());
+	for (auto const& delay : m_code.delays) {
+		m_delays.push_back(delay.initial_value);
+	}
+	if (input_providers) {
+		this->set_inputs(std::move(*input_providers));
+	}
+}
+
+void simulation::set_input(std::size_t index, input_provider_t input_provider) {
+	if (index >= m_input_functions.size()) {
+		throw py::index_error{fmt::format("Input index out of range (expected 0-{}, got {})", m_input_functions.size() - 1, index)};
+	}
+	if (auto* const callable = std::get_if<input_function_t>(&input_provider)) {
+		m_input_functions[index] = std::move(*callable);
+	} else if (auto* const numeric = std::get_if<number>(&input_provider)) {
+		m_input_functions[index] = [value = *numeric](iteration_t) -> number {
+			return value;
+		};
+	} else if (auto* const list = std::get_if<std::vector<number>>(&input_provider)) {
+		if (!m_input_length) {
+			m_input_length = static_cast<iteration_t>(list->size());
+		} else if (*m_input_length != static_cast<iteration_t>(list->size())) {
+			throw py::value_error{fmt::format("Inconsistent input length for simulation (was {}, got {})", *m_input_length, list->size())};
+		}
+		m_input_functions[index] = [values = std::move(*list)](iteration_t n) -> number {
+			return values.at(n);
+		};
+	}
+}
+
+void simulation::set_inputs(std::vector<std::optional<input_provider_t>> input_providers) {
+	if (input_providers.size() != m_input_functions.size()) {
+		throw py::value_error{fmt::format(
+			"Wrong number of inputs supplied to simulation (expected {}, got {})", m_input_functions.size(), input_providers.size())};
+	}
+	for (auto&& [i, input_provider] : enumerate(input_providers)) {
+		if (input_provider) {
+			this->set_input(i, std::move(*input_provider));
+		}
+	}
+}
+
+std::vector<number> simulation::step(bool save_results, std::optional<std::uint8_t> bits_override, bool truncate) {
+	return this->run_for(1, save_results, bits_override, truncate);
+}
+
+std::vector<number> simulation::run_until(iteration_t iteration, bool save_results, std::optional<std::uint8_t> bits_override,
+										  bool truncate) {
+	auto result = std::vector<number>{};
+	while (m_iteration < iteration) {
+		ASIC_DEBUG_MSG("Running simulation iteration.");
+		auto inputs = std::vector<number>(m_code.input_count);
+		for (auto&& [input, function] : zip(inputs, m_input_functions)) {
+			input = function(m_iteration);
+		}
+		auto state = run_simulation(m_code, inputs, m_delays, bits_override, truncate);
+		result = std::move(state.stack);
+		if (save_results) {
+			m_results.push_back(std::move(state.results));
+		}
+		++m_iteration;
+	}
+	return result;
+}
+
+std::vector<number> simulation::run_for(iteration_t iterations, bool save_results, std::optional<std::uint8_t> bits_override,
+										bool truncate) {
+	if (iterations > std::numeric_limits<iteration_t>::max() - m_iteration) {
+		throw py::value_error("Simulation iteration type overflow!");
+	}
+	return this->run_until(m_iteration + iterations, save_results, bits_override, truncate);
+}
+
+std::vector<number> simulation::run(bool save_results, std::optional<std::uint8_t> bits_override, bool truncate) {
+	if (m_input_length) {
+		return this->run_until(*m_input_length, save_results, bits_override, truncate);
+	}
+	throw py::index_error{"Tried to run unlimited simulation"};
+}
+
+iteration_t simulation::iteration() const noexcept {
+	return m_iteration;
+}
+
+pybind11::dict simulation::results() const noexcept {
+	auto results = py::dict{};
+	if (!m_results.empty()) {
+		for (auto const& [i, key] : enumerate(m_code.result_keys)) {
+			auto values = std::vector<number>{};
+			values.reserve(m_results.size());
+			for (auto const& result : m_results) {
+				values.push_back(result[i]);
+			}
+			results[py::str{key}] = py::array{static_cast<py::ssize_t>(values.size()), values.data()};
+		}
+	}
+	return results;
+}
+
+void simulation::clear_results() noexcept {
+	m_results.clear();
+}
+
+void simulation::clear_state() noexcept {
+	m_delays.clear();
+}
+
+} // namespace asic
diff --git a/src/simulation/simulation.h b/src/simulation/simulation.h
new file mode 100644
index 0000000000000000000000000000000000000000..c1a36cbc93492494af14a198c970a6a534477794
--- /dev/null
+++ b/src/simulation/simulation.h
@@ -0,0 +1,54 @@
+#ifndef ASIC_SIMULATION_DOD_H
+#define ASIC_SIMULATION_DOD_H
+
+#include "../number.h"
+#include "compile.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <functional>
+#include <optional>
+#include <pybind11/functional.h>
+#include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+#include <variant>
+#include <vector>
+
+namespace asic {
+
+using iteration_t = std::uint32_t;
+using input_function_t = std::function<number(iteration_t)>;
+using input_provider_t = std::variant<number, std::vector<number>, input_function_t>;
+
+class simulation final {
+public:
+	simulation(pybind11::handle sfg, std::optional<std::vector<std::optional<input_provider_t>>> input_providers = std::nullopt);
+
+	void set_input(std::size_t index, input_provider_t input_provider);
+	void set_inputs(std::vector<std::optional<input_provider_t>> input_providers);
+
+	[[nodiscard]] std::vector<number> step(bool save_results, std::optional<std::uint8_t> bits_override, bool truncate);
+	[[nodiscard]] std::vector<number> run_until(iteration_t iteration, bool save_results, std::optional<std::uint8_t> bits_override,
+												bool truncate);
+	[[nodiscard]] std::vector<number> run_for(iteration_t iterations, bool save_results, std::optional<std::uint8_t> bits_override,
+											  bool truncate);
+	[[nodiscard]] std::vector<number> run(bool save_results, std::optional<std::uint8_t> bits_override, bool truncate);
+
+	[[nodiscard]] iteration_t iteration() const noexcept;
+	[[nodiscard]] pybind11::dict results() const noexcept;
+
+	void clear_results() noexcept;
+	void clear_state() noexcept;
+
+private:
+	simulation_code m_code;
+	std::vector<number> m_delays;
+	std::vector<input_function_t> m_input_functions;
+	std::optional<iteration_t> m_input_length;
+	iteration_t m_iteration = 0;
+	std::vector<std::vector<number>> m_results;
+};
+
+} // namespace asic
+
+#endif // ASIC_SIMULATION_DOD_H
\ No newline at end of file
diff --git a/src/span.h b/src/span.h
new file mode 100644
index 0000000000000000000000000000000000000000..2ad454e13e3978c74355ae3ca0f955fad1bb9753
--- /dev/null
+++ b/src/span.h
@@ -0,0 +1,314 @@
+#ifndef ASIC_SPAN_H
+#define ASIC_SPAN_H
+
+#include <cstddef>
+#include <type_traits>
+#include <utility>
+#include <iterator>
+#include <limits>
+#include <array>
+#include <algorithm>
+#include <cassert>
+
+namespace asic {
+
+constexpr auto dynamic_size = static_cast<std::size_t>(-1);
+
+// C++17-compatible std::span substitute.
+template <typename T, std::size_t Size = dynamic_size>
+class span;
+
+namespace detail {
+
+template <typename T>
+struct is_span_impl : std::false_type {};
+
+template <typename T, std::size_t Size>
+struct is_span_impl<span<T, Size>> : std::true_type {};
+
+template <typename T>
+struct is_span : is_span_impl<std::remove_cv_t<T>> {};
+
+template <typename T>
+constexpr auto is_span_v = is_span<T>::value;
+
+template <typename T>
+struct is_std_array_impl : std::false_type {};
+
+template <typename T, std::size_t Size>
+struct is_std_array_impl<std::array<T, Size>> : std::true_type {};
+
+template <typename T>
+struct is_std_array : is_std_array_impl<std::remove_cv_t<T>> {};
+
+template <typename T>
+constexpr auto is_std_array_v = is_std_array<T>::value;
+
+template <std::size_t From, std::size_t To>
+struct is_size_convertible : std::bool_constant<From == To || From == dynamic_size || To == dynamic_size> {};
+
+template <std::size_t From, std::size_t To>
+constexpr auto is_size_convertible_v = is_size_convertible<From, To>::value;
+
+template <typename From, typename To>
+struct is_element_type_convertible : std::bool_constant<std::is_convertible_v<From(*)[], To(*)[]>> {};
+
+template <typename From, typename To>
+constexpr auto is_element_type_convertible_v = is_element_type_convertible<From, To>::value;
+
+template <typename T, std::size_t Size>
+struct span_base {
+	using element_type	= T;
+	using pointer		= element_type*;
+	using size_type		= std::size_t;
+
+	constexpr span_base() noexcept = default;
+	constexpr span_base(pointer data, [[maybe_unused]] size_type size) : m_data(data) { assert(size == Size); }
+
+	template <size_type N>
+	constexpr span_base(span_base<T, N> other) : m_data(other.data()) {
+		static_assert(N == Size || N == dynamic_size);
+		assert(other.size() == Size);
+	}
+
+	[[nodiscard]] constexpr pointer data() const noexcept	{ return m_data; }
+	[[nodiscard]] constexpr size_type size() const noexcept	{ return Size; }
+
+private:
+	pointer m_data = nullptr;
+};
+
+template <typename T>
+struct span_base<T, dynamic_size> {
+	using element_type	= T;
+	using pointer		= element_type*;
+	using size_type		= std::size_t;
+
+	constexpr span_base() noexcept = default;
+	constexpr span_base(pointer data, size_type size) : m_data(data), m_size(size) {}
+
+	template <size_type N>
+	explicit constexpr span_base(span_base<T, N> other) : m_data(other.data()), m_size(other.size()) {}
+
+	[[nodiscard]] constexpr pointer data() const noexcept	{ return m_data; }
+	[[nodiscard]] constexpr size_type size() const noexcept	{ return m_size; }
+
+private:
+	pointer		m_data = nullptr;
+	size_type	m_size = 0;
+};
+
+template <typename T, std::size_t Size, std::size_t Offset, std::size_t N>
+struct subspan_type {
+	using type = span<
+		T,
+		(N != dynamic_size) ?
+			N :
+			(Size != dynamic_size) ?
+				Size - Offset :
+				Size
+	>;
+};
+
+template <typename T, std::size_t Size, std::size_t Offset, std::size_t Count>
+using subspan_type_t = typename subspan_type<T, Size, Offset, Count>::type;
+
+} // namespace detail
+
+template <typename T, std::size_t Size>
+class span final : public detail::span_base<T, Size> {
+public:
+	using element_type				= typename detail::span_base<T, Size>::element_type;
+	using pointer					= typename detail::span_base<T, Size>::pointer;
+	using size_type					= typename detail::span_base<T, Size>::size_type;
+	using value_type				= std::remove_cv_t<element_type>;
+	using reference					= element_type&;
+	using iterator					= element_type*;
+	using const_iterator			= const element_type*;
+	using reverse_iterator			= std::reverse_iterator<iterator>;
+	using const_reverse_iterator	= std::reverse_iterator<const_iterator>;
+
+	// Default constructor.
+	constexpr span() noexcept = default;
+
+	// Construct from pointer, size.
+	constexpr span(pointer data, size_type size) : detail::span_base<T, Size>(data, size) {}
+
+	// Copy constructor.
+	template <
+		typename U, std::size_t N,
+		typename = std::enable_if_t<detail::is_size_convertible_v<N, Size>>,
+		typename = std::enable_if_t<detail::is_element_type_convertible_v<U, T>>
+	>
+	constexpr span(span<U, N> const& other) : span(other.data(), other.size()) {}
+
+	// Copy assignment.
+	constexpr span& operator=(span const&) noexcept = default;
+
+	// Destructor.
+	~span() = default;
+
+	// Construct from begin, end.
+	constexpr span(pointer begin, pointer end) : span(begin, end - begin) {}
+
+	// Construct from C array.
+	template <std::size_t N>
+	constexpr span(element_type(&arr)[N]) noexcept : span(std::data(arr), N) {}
+
+	// Construct from std::array.
+	template <
+		std::size_t N,
+		typename = std::enable_if_t<N != 0>
+	>
+	constexpr span(std::array<value_type, N>& arr) noexcept : span(std::data(arr), N) {}
+
+	// Construct from empty std::array.
+	constexpr span(std::array<value_type, 0>&) noexcept : span() {}
+
+	// Construct from const std::array.
+	template <
+		std::size_t N,
+		typename = std::enable_if_t<N != 0>
+	>
+	constexpr span(std::array<value_type, N> const& arr) noexcept : span(std::data(arr), N) {}
+
+	// Construct from empty const std::array.
+	constexpr span(std::array<value_type, 0> const&) noexcept : span() {}
+
+	// Construct from other container.
+	template <
+		typename Container,
+		typename = std::enable_if_t<!detail::is_span_v<Container>>,
+		typename = std::enable_if_t<!detail::is_std_array_v<Container>>,
+		typename = decltype(std::data(std::declval<Container>())),
+		typename = decltype(std::size(std::declval<Container>())),
+		typename = std::enable_if_t<std::is_convertible_v<typename Container::pointer, pointer>>,
+		typename = std::enable_if_t<std::is_convertible_v<typename Container::pointer, decltype(std::data(std::declval<Container>()))>>
+	>
+	constexpr span(Container& container) : span(std::data(container), std::size(container)) {}
+
+	// Construct from other const container.
+	template <
+		typename Container,
+		typename Element = element_type,
+		typename = std::enable_if_t<std::is_const_v<Element>>,
+		typename = std::enable_if_t<!detail::is_span_v<Container>>,
+		typename = std::enable_if_t<!detail::is_std_array_v<Container>>,
+		typename = decltype(std::data(std::declval<Container>())),
+		typename = decltype(std::size(std::declval<Container>())),
+		typename = std::enable_if_t<std::is_convertible_v<typename Container::pointer, pointer>>,
+		typename = std::enable_if_t<std::is_convertible_v<typename Container::pointer, decltype(std::data(std::declval<Container>()))>>
+	>
+	constexpr span(Container const& container) : span(std::data(container), std::size(container)) {}
+
+	[[nodiscard]] constexpr iterator begin() const noexcept						{ return this->data(); }
+	[[nodiscard]] constexpr const_iterator cbegin() const noexcept				{ return this->data(); }
+	[[nodiscard]] constexpr iterator end() const noexcept						{ return this->data() + this->size(); }
+	[[nodiscard]] constexpr const_iterator cend() const noexcept				{ return this->data() + this->size(); }
+	[[nodiscard]] constexpr reverse_iterator rbegin() const noexcept			{ return std::make_reverse_iterator(this->end()); }
+	[[nodiscard]] constexpr const_reverse_iterator crbegin() const noexcept		{ return std::make_reverse_iterator(this->cend()); }
+	[[nodiscard]] constexpr reverse_iterator rend() const noexcept				{ return std::make_reverse_iterator(this->begin()); }
+	[[nodiscard]] constexpr const_reverse_iterator crend() const noexcept		{ return std::make_reverse_iterator(this->cbegin()); }
+
+	[[nodiscard]] constexpr reference operator[](size_type i) const noexcept	{ assert(i < this->size()); return this->data()[i]; }
+	[[nodiscard]] constexpr reference operator()(size_type i) const noexcept	{ assert(i < this->size()); return this->data()[i]; }
+
+	[[nodiscard]] constexpr size_type size_bytes() const noexcept				{ return this->size() * sizeof(element_type); }
+	[[nodiscard]] constexpr bool empty() const noexcept							{ return this->size() == 0; }
+
+	[[nodiscard]] constexpr reference front() const noexcept					{ assert(!this->empty()); return this->data()[0]; }
+	[[nodiscard]] constexpr reference back() const noexcept						{ assert(!this->empty()); return this->data()[this->size() - 1]; }
+
+	template <std::size_t N>
+	[[nodiscard]] constexpr span<T, N> first() const {
+		static_assert(N != dynamic_size && N <= Size);
+		return {this->data(), N};
+	}
+
+	template <std::size_t N>
+	[[nodiscard]] constexpr span<T, N> last() const {
+		static_assert(N != dynamic_size && N <= Size);
+		return {this->data() + (Size - N), N};
+	}
+
+	template <std::size_t Offset, std::size_t N = dynamic_size>
+	[[nodiscard]] constexpr auto subspan() const -> detail::subspan_type_t<T, Size, Offset, N> {
+		static_assert(Offset <= Size);
+		return {this->data() + Offset, (N == dynamic_size) ? this->size() - Offset : N};
+	}
+
+	[[nodiscard]] constexpr span<T, dynamic_size> first(size_type n) const {
+		assert(n <= this->size());
+		return { this->data(), n };
+	}
+
+	[[nodiscard]] constexpr span<T, dynamic_size> last(size_type n) const {
+		return this->subspan(this->size() - n);
+	}
+
+	[[nodiscard]] constexpr span<T, dynamic_size> subspan(size_type offset, size_type n = dynamic_size) const {
+		if constexpr (Size == dynamic_size) {
+			assert(offset <= this->size());
+			if (n == dynamic_size) {
+				return { this->data() + offset, this->size() - offset };
+			}
+			assert(n <= this->size());
+			assert(offset + n <= this->size());
+			return {this->data() + offset, n};
+		} else {
+			return span<T, dynamic_size>{*this}.subspan(offset, n);
+		}
+	}
+};
+
+template <typename T, std::size_t LhsSize, std::size_t RhsSize>
+[[nodiscard]] constexpr bool operator==(span<T, LhsSize> lhs, span<T, RhsSize> rhs) {
+	return std::equal(lhs.begin(), lhs.end(), rhs.begin(), rhs.end());
+}
+
+template <typename T, std::size_t LhsSize, std::size_t RhsSize>
+[[nodiscard]] constexpr bool operator!=(span<T, LhsSize> lhs, span<T, RhsSize> rhs) {
+	return !(lhs == rhs);
+}
+
+template <typename T, std::size_t LhsSize, std::size_t RhsSize>
+[[nodiscard]] constexpr bool operator<(span<T, LhsSize> lhs, span<T, RhsSize> rhs) {
+	return std::lexicographical_compare(lhs.begin(), lhs.end(), rhs.begin(), rhs.end());
+}
+
+template <typename T, std::size_t LhsSize, std::size_t RhsSize>
+[[nodiscard]] constexpr bool operator<=(span<T, LhsSize> lhs, span<T, RhsSize> rhs) {
+	return !(lhs > rhs);
+}
+
+template <typename T, std::size_t LhsSize, std::size_t RhsSize>
+[[nodiscard]] constexpr bool operator>(span<T, LhsSize> lhs, span<T, RhsSize> rhs) {
+	return rhs < lhs;
+}
+
+template <typename T, std::size_t LhsSize, std::size_t RhsSize>
+[[nodiscard]] constexpr bool operator>=(span<T, LhsSize> lhs, span<T, RhsSize> rhs) {
+	return !(lhs < rhs);
+}
+
+template <typename Container>
+span(Container&) -> span<typename Container::value_type>;
+
+template <typename Container>
+span(Container const&) -> span<typename Container::value_type const>;
+
+template <typename T, std::size_t N>
+span(T(&)[N]) -> span<T, N>;
+
+template <typename T, std::size_t N>
+span(std::array<T, N>&) -> span<T, N>;
+
+template <typename T, std::size_t N>
+span(std::array<T, N> const&) -> span<T const, N>;
+
+template <typename T, typename Dummy>
+span(T, Dummy&&) -> span<std::remove_reference_t<decltype(std::declval<T>()[0])>>;
+
+} // namespace asic
+
+#endif // ASIC_SPAN_H
\ No newline at end of file
diff --git a/test/conftest.py b/test/conftest.py
index 64f39843c53a4369781a269fd7fc30ad9aa1d255..63cf5ce1d0eb3d6652dc4eee14113aba98a3f2fc 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -1,4 +1,5 @@
 from test.fixtures.signal import signal, signals
 from test.fixtures.operation_tree import *
 from test.fixtures.port import *
-import pytest
+from test.fixtures.signal_flow_graph import *
+import pytest
\ No newline at end of file
diff --git a/test/fixtures/operation_tree.py b/test/fixtures/operation_tree.py
index df3fcac35cc495d14bed06ccdfc2a3ebed25616e..695979c65ab56eda3baf992b3b99963ee1fe7c9a 100644
--- a/test/fixtures/operation_tree.py
+++ b/test/fixtures/operation_tree.py
@@ -1,58 +1,95 @@
-from b_asic.core_operations import Addition, Constant
-from b_asic.signal import Signal
-
 import pytest
 
+from b_asic import Addition, Constant, Signal, Butterfly
+
+
 @pytest.fixture
 def operation():
     return Constant(2)
 
-def create_operation(_type, dest_oper, index, **kwargs):
-    oper = _type(**kwargs)
-    oper_signal = Signal()
-    oper._output_ports[0].add_signal(oper_signal)
-
-    dest_oper._input_ports[index].add_signal(oper_signal)
-    return oper
-
 @pytest.fixture
 def operation_tree():
-    """Return a addition operation connected with 2 constants.
-    ---C---+
-           ---A
-    ---C---+
+    """Valid addition operation connected with 2 constants.
+    2---+
+        |
+        v
+       add = 2 + 3 = 5
+        ^
+        |
+    3---+
     """
-    add_oper = Addition()
-    create_operation(Constant, add_oper, 0, value=2)
-    create_operation(Constant, add_oper, 1, value=3)
-    return add_oper
+    return Addition(Constant(2), Constant(3))
 
 @pytest.fixture
 def large_operation_tree():
-    """Return a constant operation connected with a large operation tree with 3 other constants and 3 additions.
-    ---C---+
-           ---A---+
-    ---C---+      |
-                  +---A
-    ---C---+      |
-           ---A---+
-    ---C---+
+    """Valid addition operation connected with a large operation tree with 2 other additions and 4 constants.
+    2---+
+        |
+        v
+       add---+
+        ^    |
+        |    |
+    3---+    v
+            add = (2 + 3) + (4 + 5) = 14
+    4---+    ^
+        |    |
+        v    |
+       add---+
+        ^
+        |
+    5---+
     """
-    add_oper = Addition()
-    add_oper_2 = Addition()
-
-    const_oper = create_operation(Constant, add_oper, 0, value=2)
-    create_operation(Constant, add_oper, 1, value=3)
+    return Addition(Addition(Constant(2), Constant(3)), Addition(Constant(4), Constant(5)))
 
-    create_operation(Constant, add_oper_2, 0, value=4)
-    create_operation(Constant, add_oper_2, 1, value=5)
+@pytest.fixture
+def large_operation_tree_names():
+    """Valid addition operation connected with a large operation tree with 2 other additions and 4 constants.
+    With names.
+    2---+
+        |
+        v
+       add---+
+        ^    |
+        |    |
+    3---+    v
+            add = (2 + 3) + (4 + 5) = 14
+    4---+    ^
+        |    |
+        v    |
+       add---+
+        ^
+        |
+    5---+
+    """
+    return Addition(Addition(Constant(2, name="constant2"), Constant(3, name="constant3")), Addition(Constant(4, name="constant4"), Constant(5, name="constant5")))
 
-    add_oper_3 = Addition()
-    add_oper_signal = Signal(add_oper.output(0), add_oper_3.output(0))
-    add_oper._output_ports[0].add_signal(add_oper_signal)
-    add_oper_3._input_ports[0].add_signal(add_oper_signal)
+@pytest.fixture
+def butterfly_operation_tree():
+    """Valid butterfly operations connected to eachother with 3 butterfly operations and 2 constants as inputs and 2 outputs.
+    2 ---+       +--- (2 + 4) ---+       +--- (6 + (-2)) ---+       +--- (4 + 8) ---> out1 = 12
+         |       |               |       |                  |       |
+         v       ^               v       ^                  v       ^
+         butterfly               butterfly                  butterfly
+         ^       v               ^       v                  ^       v
+         |       |               |       |                  |       |               
+    4 ---+       +--- (2 - 4) ---+       +--- (6 - (-2)) ---+       +--- (4 - 8) ---> out2 = -4
+    """
+    return Butterfly(*(Butterfly(*(Butterfly(Constant(2), Constant(4), name="bfly3").outputs), name="bfly2").outputs), name="bfly1")
 
-    add_oper_2_signal = Signal(add_oper_2.output(0), add_oper_3.output(0))
-    add_oper_2._output_ports[0].add_signal(add_oper_2_signal)
-    add_oper_3._input_ports[1].add_signal(add_oper_2_signal)
-    return const_oper
+@pytest.fixture
+def operation_graph_with_cycle():
+    """Invalid addition operation connected with an operation graph containing a cycle.
+     +-+
+     | |
+     v |
+    add+---+
+     ^     |
+     |     v
+     7    add = (? + 7) + 6 = ?
+           ^
+           |
+           6
+    """
+    add1 = Addition(None, Constant(7))
+    add1.input(0).connect(add1)
+    return Addition(add1, Constant(6))
diff --git a/test/fixtures/port.py b/test/fixtures/port.py
index 4019b3a2016aa418daeca771f9a2d8bcc4ca6652..4cce4f69b1f11b44426d1bd39702dba4e11c0efe 100644
--- a/test/fixtures/port.py
+++ b/test/fixtures/port.py
@@ -1,10 +1,20 @@
 import pytest
-from b_asic.port import InputPort, OutputPort
+
+from b_asic import InputPort, OutputPort
+
 
 @pytest.fixture
 def input_port():
-    return InputPort(0, None)
+    return InputPort(None, 0)
 
 @pytest.fixture
 def output_port():
-    return OutputPort(0, None)
+    return OutputPort(None, 0)
+
+@pytest.fixture
+def list_of_input_ports():
+    return [InputPort(None, i) for i in range(0, 3)]
+
+@pytest.fixture
+def list_of_output_ports():
+    return [OutputPort(None, i) for i in range(0, 3)]
diff --git a/test/fixtures/signal.py b/test/fixtures/signal.py
index 7b13c9789f94c33338d08b48373391398bd9f71d..4dba99e24bce16aba67cba58057b3cde76f0923d 100644
--- a/test/fixtures/signal.py
+++ b/test/fixtures/signal.py
@@ -1,6 +1,8 @@
 import pytest
+
 from b_asic import Signal
 
+
 @pytest.fixture
 def signal():
     """Return a signal with no connections."""
@@ -9,4 +11,4 @@ def signal():
 @pytest.fixture
 def signals():
     """Return 3 signals with no connections."""
-    return [Signal() for _ in range(0,3)]
+    return [Signal() for _ in range(0, 3)]
diff --git a/test/fixtures/signal_flow_graph.py b/test/fixtures/signal_flow_graph.py
new file mode 100644
index 0000000000000000000000000000000000000000..a2c25ec9b10083b08c46543c7a5bfc30607560ef
--- /dev/null
+++ b/test/fixtures/signal_flow_graph.py
@@ -0,0 +1,245 @@
+import pytest
+
+from b_asic import SFG, Input, Output, Constant, Delay, Addition, ConstantMultiplication, Butterfly, AbstractOperation, Name, TypeName, SignalSourceProvider
+from typing import Optional
+
+
+@pytest.fixture
+def sfg_two_inputs_two_outputs():
+    """Valid SFG with two inputs and two outputs.
+         .               .
+    in1-------+  +--------->out1
+         .    |  |       .
+         .    v  |       .
+         .   add1+--+    .
+         .    ^     |    .
+         .    |     v    .
+    in2+------+    add2---->out2
+       | .          ^    .
+       | .          |    .
+       +------------+    .
+         .               .
+    out1 = in1 + in2
+    out2 = in1 + 2 * in2
+    """
+    in1 = Input("IN1")
+    in2 = Input("IN2")
+    add1 = Addition(in1, in2, "ADD1")
+    add2 = Addition(add1, in2, "ADD2")
+    out1 = Output(add1, "OUT1")
+    out2 = Output(add2, "OUT2")
+    return SFG(inputs=[in1, in2], outputs=[out1, out2])
+
+
+@pytest.fixture
+def sfg_two_inputs_two_outputs_independent():
+    """Valid SFG with two inputs and two outputs, where the first output only depends
+    on the first input and the second output only depends on the second input.
+         .               .
+    in1-------------------->out1
+         .               .
+         .               .
+         .      c1--+    .
+         .          |    .
+         .          v    .
+    in2------+     add1---->out2
+         .   |      ^    .
+         .   |      |    .
+         .   +------+    .
+         .               .
+    out1 = in1
+    out2 = in2 + 3
+    """
+    in1 = Input("IN1")
+    in2 = Input("IN2")
+    c1 = Constant(3, "C1")
+    add1 = Addition(in2, c1, "ADD1")
+    out1 = Output(in1, "OUT1")
+    out2 = Output(add1, "OUT2")
+    return SFG(inputs=[in1, in2], outputs=[out1, out2])
+
+
+@pytest.fixture
+def sfg_two_inputs_two_outputs_independent_with_cmul():
+    """Valid SFG with two inputs and two outputs, where the first output only depends
+    on the first input and the second output only depends on the second input.
+        .                 .
+    in1--->cmul1--->cmul2--->out1
+        .                 .
+        .                 .
+        .  c1             .
+        .   |             .
+        .   v             .
+    in2--->add1---->cmul3--->out2
+        .                 .
+    """
+    in1 = Input("IN1")
+    in2 = Input("IN2")
+    c1 = Constant(3, "C1")
+    add1 = Addition(in2, c1, "ADD1", 7)
+    cmul3 = ConstantMultiplication(2, add1, "CMUL3", 3)
+    cmul1 = ConstantMultiplication(5, in1, "CMUL1", 5)
+    cmul2 = ConstantMultiplication(4, cmul1, "CMUL2", 4)
+    out1 = Output(cmul2, "OUT1")
+    out2 = Output(cmul3, "OUT2")
+    return SFG(inputs=[in1, in2], outputs=[out1, out2])
+
+
+@pytest.fixture
+def sfg_nested():
+    """Valid SFG with two inputs and one output.
+    out1 = in1 + (in1 + in1 * in2) * (in1 + in2 * (in1 + in1 * in2))
+    """
+    mac_in1 = Input()
+    mac_in2 = Input()
+    mac_in3 = Input()
+    mac_out1 = Output(mac_in1 + mac_in2 * mac_in3)
+    MAC = SFG(inputs=[mac_in1, mac_in2, mac_in3], outputs=[mac_out1])
+
+    in1 = Input()
+    in2 = Input()
+    mac1 = MAC(in1, in1, in2)
+    mac2 = MAC(in1, in2, mac1)
+    mac3 = MAC(in1, mac1, mac2)
+    out1 = Output(mac3)
+    return SFG(inputs=[in1, in2], outputs=[out1])
+
+
+@pytest.fixture
+def sfg_delay():
+    """Valid SFG with one input and one output.
+    out1 = in1'
+    """
+    in1 = Input()
+    t1 = Delay(in1)
+    out1 = Output(t1)
+    return SFG(inputs = [in1], outputs = [out1])
+
+@pytest.fixture
+def sfg_accumulator():
+    """Valid SFG with two inputs and one output.
+    data_out = (data_in' + data_in) * (1 - reset)
+    """
+    data_in = Input()
+    reset = Input()
+    t = Delay()
+    t << (t + data_in) * (1 - reset)
+    data_out = Output(t)
+    return SFG(inputs = [data_in, reset], outputs = [data_out])
+
+
+@pytest.fixture
+def sfg_simple_accumulator():
+    """Valid SFG with two inputs and one output.
+         .                .
+    in1----->add1-----+----->out1
+         .    ^       |   .
+         .    |       |   .
+         .    +--t1<--+   .
+         .                .
+    """
+    in1 = Input()
+    t1 = Delay()
+    add1 = in1 + t1
+    t1 << add1
+    out1 = Output(add1)
+    return SFG(inputs = [in1], outputs = [out1])
+
+@pytest.fixture
+def sfg_simple_filter():
+    """A valid SFG that is used as a filter in the first lab for TSTE87.
+         .                 .
+         .   +--cmul1<--+  .
+         .   |          |  .
+         .   v          |  .
+    in1---->add1----->t1+---->out1
+         .                 .
+    """
+    in1 = Input("IN1")
+    cmul1 = ConstantMultiplication(0.5, name="CMUL1")
+    add1 = Addition(in1, cmul1, "ADD1")
+    add1.input(1).signals[0].name = "S2"
+    t1 = Delay(add1, name="T1")
+    cmul1.input(0).connect(t1, "S1")
+    out1 = Output(t1, "OUT1")
+    return SFG(inputs=[in1], outputs=[out1], name="simple_filter")
+
+@pytest.fixture
+def sfg_custom_operation():
+    """A valid SFG containing a custom operation."""
+    class CustomOperation(AbstractOperation):
+        def __init__(self, src0: Optional[SignalSourceProvider] = None, name: Name = ""):
+            super().__init__(input_count = 1, output_count = 2, name = name, input_sources = [src0])
+        
+        @classmethod
+        def type_name(self) -> TypeName:
+            return "custom"
+
+        def evaluate(self, a):
+            return a * 2, 2 ** a
+
+    in1 = Input()
+    custom1 = CustomOperation(in1)
+    out1 = Output(custom1.output(0))
+    out2 = Output(custom1.output(1))
+    return SFG(inputs=[in1], outputs=[out1, out2])
+
+
+@pytest.fixture
+def precedence_sfg_delays():
+    """A sfg with delays and interesting layout for precednce list generation.
+         .                                          .
+    IN1>--->C0>--->ADD1>--->Q1>---+--->A0>--->ADD4>--->OUT1
+         .           ^            |            ^    .
+         .           |            T1           |    .
+         .           |            |            |    .
+         .         ADD2<---<B1<---+--->A1>--->ADD3  .
+         .           ^            |            ^    .
+         .           |            T2           |    .
+         .           |            |            |    .
+         .           +-----<B2<---+--->A2>-----+    .
+    """
+    in1 = Input("IN1")
+    c0 = ConstantMultiplication(5, in1, "C0")
+    add1 = Addition(c0, None, "ADD1")
+    # Not sure what operation "Q" is supposed to be in the example
+    Q1 = ConstantMultiplication(1, add1, "Q1")
+    T1 = Delay(Q1, 0, "T1")
+    T2 = Delay(T1, 0, "T2")
+    b2 = ConstantMultiplication(2, T2, "B2")
+    b1 = ConstantMultiplication(3, T1, "B1")
+    add2 = Addition(b1, b2, "ADD2")
+    add1.input(1).connect(add2)
+    a1 = ConstantMultiplication(4, T1, "A1")
+    a2 = ConstantMultiplication(6, T2, "A2")
+    add3 = Addition(a1, a2, "ADD3")
+    a0 = ConstantMultiplication(7, Q1, "A0")
+    add4 = Addition(a0, add3, "ADD4")
+    out1 = Output(add4, "OUT1")
+
+    return SFG(inputs=[in1], outputs=[out1], name="SFG")
+
+
+@pytest.fixture
+def precedence_sfg_delays_and_constants():
+    in1 = Input("IN1")
+    c0 = ConstantMultiplication(5, in1, "C0")
+    add1 = Addition(c0, None, "ADD1")
+    # Not sure what operation "Q" is supposed to be in the example
+    Q1 = ConstantMultiplication(1, add1, "Q1")
+    T1 = Delay(Q1, 0, "T1")
+    const1 = Constant(10, "CONST1")  # Replace T2 delay with a constant
+    b2 = ConstantMultiplication(2, const1, "B2")
+    b1 = ConstantMultiplication(3, T1, "B1")
+    add2 = Addition(b1, b2, "ADD2")
+    add1.input(1).connect(add2)
+    a1 = ConstantMultiplication(4, T1, "A1")
+    a2 = ConstantMultiplication(10, const1, "A2")
+    add3 = Addition(a1, a2, "ADD3")
+    a0 = ConstantMultiplication(7, Q1, "A0")
+    # Replace ADD4 with a butterfly to test multiple output ports
+    bfly1 = Butterfly(a0, add3, "BFLY1")
+    out1 = Output(bfly1.output(0), "OUT1")
+    Output(bfly1.output(1), "OUT2")
+
+    return SFG(inputs=[in1], outputs=[out1], name="SFG")
diff --git a/test/operation/test_abstract_operation.py b/test/operation/test_abstract_operation.py
deleted file mode 100644
index 626a2dc3e5e26fb76d9266dcdd31940681df5c6e..0000000000000000000000000000000000000000
--- a/test/operation/test_abstract_operation.py
+++ /dev/null
@@ -1,77 +0,0 @@
-"""
-B-ASIC test suite for the AbstractOperation class.
-"""
-
-from b_asic.core_operations import Addition, ConstantAddition, Subtraction, ConstantSubtraction, \
-    Multiplication, ConstantMultiplication, Division, ConstantDivision
-
-import pytest
-
-
-def test_addition_overload():
-    """Tests addition overloading for both operation and number argument."""
-    add1 = Addition(None, None, "add1")
-    add2 = Addition(None, None, "add2")
-
-    add3 = add1 + add2
-
-    assert isinstance(add3, Addition)
-    assert add3.input(0).signals == add1.output(0).signals
-    assert add3.input(1).signals == add2.output(0).signals
-
-    add4 = add3 + 5
-
-    assert isinstance(add4, ConstantAddition)
-    assert add4.input(0).signals == add3.output(0).signals
-
-
-def test_subtraction_overload():
-    """Tests subtraction overloading for both operation and number argument."""
-    add1 = Addition(None, None, "add1")
-    add2 = Addition(None, None, "add2")
-
-    sub1 = add1 - add2
-
-    assert isinstance(sub1, Subtraction)
-    assert sub1.input(0).signals == add1.output(0).signals
-    assert sub1.input(1).signals == add2.output(0).signals
-
-    sub2 = sub1 - 5
-
-    assert isinstance(sub2, ConstantSubtraction)
-    assert sub2.input(0).signals == sub1.output(0).signals
-
-
-def test_multiplication_overload():
-    """Tests multiplication overloading for both operation and number argument."""
-    add1 = Addition(None, None, "add1")
-    add2 = Addition(None, None, "add2")
-
-    mul1 = add1 * add2
-
-    assert isinstance(mul1, Multiplication)
-    assert mul1.input(0).signals == add1.output(0).signals
-    assert mul1.input(1).signals == add2.output(0).signals
-
-    mul2 = mul1 * 5
-
-    assert isinstance(mul2, ConstantMultiplication)
-    assert mul2.input(0).signals == mul1.output(0).signals
-
-
-def test_division_overload():
-    """Tests division overloading for both operation and number argument."""
-    add1 = Addition(None, None, "add1")
-    add2 = Addition(None, None, "add2")
-
-    div1 = add1 / add2
-
-    assert isinstance(div1, Division)
-    assert div1.input(0).signals == add1.output(0).signals
-    assert div1.input(1).signals == add2.output(0).signals
-
-    div2 = div1 / 5
-
-    assert isinstance(div2, ConstantDivision)
-    assert div2.input(0).signals == div1.output(0).signals
-
diff --git a/test/test_core_operations.py b/test/test_core_operations.py
index b176b2a6506cc5a1297813f6ddcb6d3589492838..6a0493c60965579bd843e0b514bd7f9b9a0e4707 100644
--- a/test/test_core_operations.py
+++ b/test/test_core_operations.py
@@ -2,226 +2,175 @@
 B-ASIC test suite for the core operations.
 """
 
-from b_asic.core_operations import Constant, Addition, Subtraction, Multiplication, Division, SquareRoot, ComplexConjugate, Max, Min, Absolute, ConstantMultiplication, ConstantAddition, ConstantSubtraction, ConstantDivision
-
-# Constant tests.
-def test_constant():
-    constant_operation = Constant(3)
-    assert constant_operation.evaluate() == 3
-
-def test_constant_negative():
-    constant_operation = Constant(-3)
-    assert constant_operation.evaluate() == -3
-
-def test_constant_complex():
-    constant_operation = Constant(3+4j)
-    assert constant_operation.evaluate() == 3+4j
-
-# Addition tests.
-def test_addition():
-    test_operation = Addition()
-    constant_operation = Constant(3)
-    constant_operation_2 = Constant(5)
-    assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == 8
-
-def test_addition_negative():
-    test_operation = Addition()
-    constant_operation = Constant(-3)
-    constant_operation_2 = Constant(-5)
-    assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == -8
-
-def test_addition_complex():
-    test_operation = Addition()
-    constant_operation = Constant((3+5j))
-    constant_operation_2 = Constant((4+6j))
-    assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == (7+11j)
-
-# Subtraction tests.
-def test_subtraction():
-    test_operation = Subtraction()
-    constant_operation = Constant(5)
-    constant_operation_2 = Constant(3)
-    assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == 2
-
-def test_subtraction_negative():
-    test_operation = Subtraction()
-    constant_operation = Constant(-5)
-    constant_operation_2 = Constant(-3)
-    assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == -2
-
-def test_subtraction_complex():
-    test_operation = Subtraction()
-    constant_operation = Constant((3+5j))
-    constant_operation_2 = Constant((4+6j))
-    assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == (-1-1j)
-
-# Multiplication tests.
-def test_multiplication():
-    test_operation = Multiplication()
-    constant_operation = Constant(5)
-    constant_operation_2 = Constant(3)
-    assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == 15
-
-def test_multiplication_negative():
-    test_operation = Multiplication()
-    constant_operation = Constant(-5)
-    constant_operation_2 = Constant(-3)
-    assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == 15
-
-def test_multiplication_complex():
-    test_operation = Multiplication()
-    constant_operation = Constant((3+5j))
-    constant_operation_2 = Constant((4+6j))
-    assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == (-18+38j)
-
-# Division tests.
-def test_division():
-    test_operation = Division()
-    constant_operation = Constant(30)
-    constant_operation_2 = Constant(5)
-    assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == 6
-
-def test_division_negative():
-    test_operation = Division()
-    constant_operation = Constant(-30)
-    constant_operation_2 = Constant(-5)
-    assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == 6
-
-def test_division_complex():
-    test_operation = Division()
-    constant_operation = Constant((60+40j))
-    constant_operation_2 = Constant((10+20j))
-    assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == (2.8-1.6j)
-
-# SquareRoot tests.
-def test_squareroot():
-    test_operation = SquareRoot()
-    constant_operation = Constant(36)
-    assert test_operation.evaluate(constant_operation.evaluate()) == 6
-
-def test_squareroot_negative():
-    test_operation = SquareRoot()
-    constant_operation = Constant(-36)
-    assert test_operation.evaluate(constant_operation.evaluate()) == 6j
-
-def test_squareroot_complex():
-    test_operation = SquareRoot()
-    constant_operation = Constant((48+64j))
-    assert test_operation.evaluate(constant_operation.evaluate()) == (8+4j)
-
-# ComplexConjugate tests.
-def test_complexconjugate():
-    test_operation = ComplexConjugate()
-    constant_operation = Constant(3+4j)
-    assert test_operation.evaluate(constant_operation.evaluate()) == (3-4j)
-
-def test_test_complexconjugate_negative():
-    test_operation = ComplexConjugate()
-    constant_operation = Constant(-3-4j)
-    assert test_operation.evaluate(constant_operation.evaluate()) == (-3+4j)
-
-# Max tests.
-def test_max():
-    test_operation = Max()
-    constant_operation = Constant(30)
-    constant_operation_2 = Constant(5)
-    assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == 30
-
-def test_max_negative():
-    test_operation = Max()
-    constant_operation = Constant(-30)
-    constant_operation_2 = Constant(-5)
-    assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == -5
-
-# Min tests.
-def test_min():
-    test_operation = Min()
-    constant_operation = Constant(30)
-    constant_operation_2 = Constant(5)
-    assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == 5
-
-def test_min_negative():
-    test_operation = Min()
-    constant_operation = Constant(-30)
-    constant_operation_2 = Constant(-5)
-    assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == -30
-
-# Absolute tests.
-def test_absolute():
-    test_operation = Absolute()
-    constant_operation = Constant(30)
-    assert test_operation.evaluate(constant_operation.evaluate()) == 30
-
-def test_absolute_negative():
-    test_operation = Absolute()
-    constant_operation = Constant(-5)
-    assert test_operation.evaluate(constant_operation.evaluate()) == 5
-
-def test_absolute_complex():
-    test_operation = Absolute()
-    constant_operation = Constant((3+4j))
-    assert test_operation.evaluate(constant_operation.evaluate()) == 5.0
-
-# ConstantMultiplication tests.
-def test_constantmultiplication():
-    test_operation = ConstantMultiplication(5)
-    constant_operation = Constant(20)
-    assert test_operation.evaluate(constant_operation.evaluate()) == 100
-
-def test_constantmultiplication_negative():
-    test_operation = ConstantMultiplication(5)
-    constant_operation = Constant(-5)
-    assert test_operation.evaluate(constant_operation.evaluate()) == -25
-
-def test_constantmultiplication_complex():
-    test_operation = ConstantMultiplication(3+2j)
-    constant_operation = Constant((3+4j))
-    assert test_operation.evaluate(constant_operation.evaluate()) == (1+18j)
-
-# ConstantAddition tests.
-def test_constantaddition():
-    test_operation = ConstantAddition(5)
-    constant_operation = Constant(20)
-    assert test_operation.evaluate(constant_operation.evaluate()) == 25
-
-def test_constantaddition_negative():
-    test_operation = ConstantAddition(4)
-    constant_operation = Constant(-5)
-    assert test_operation.evaluate(constant_operation.evaluate()) == -1
-
-def test_constantaddition_complex():
-    test_operation = ConstantAddition(3+2j)
-    constant_operation = Constant((3+4j))
-    assert test_operation.evaluate(constant_operation.evaluate()) == (6+6j)
-
-# ConstantSubtraction tests.
-def test_constantsubtraction():
-    test_operation = ConstantSubtraction(5)
-    constant_operation = Constant(20)
-    assert test_operation.evaluate(constant_operation.evaluate()) == 15
-
-def test_constantsubtraction_negative():
-    test_operation = ConstantSubtraction(4)
-    constant_operation = Constant(-5)
-    assert test_operation.evaluate(constant_operation.evaluate()) == -9
-
-def test_constantsubtraction_complex():
-    test_operation = ConstantSubtraction(4+6j)
-    constant_operation = Constant((3+4j))
-    assert test_operation.evaluate(constant_operation.evaluate()) == (-1-2j)
-
-# ConstantDivision tests.
-def test_constantdivision():
-    test_operation = ConstantDivision(5)
-    constant_operation = Constant(20)
-    assert test_operation.evaluate(constant_operation.evaluate()) == 4
-
-def test_constantdivision_negative():
-    test_operation = ConstantDivision(4)
-    constant_operation = Constant(-20)
-    assert test_operation.evaluate(constant_operation.evaluate()) == -5
-
-def test_constantdivision_complex():
-    test_operation = ConstantDivision(2+2j)
-    constant_operation = Constant((10+10j))
-    assert test_operation.evaluate(constant_operation.evaluate()) == (5+0j)
+from b_asic import \
+    Constant, Addition, Subtraction, Multiplication, ConstantMultiplication, Division, \
+    SquareRoot, ComplexConjugate, Max, Min, Absolute, Butterfly
+
+class TestConstant:
+    def test_constant_positive(self):
+        test_operation = Constant(3)
+        assert test_operation.evaluate_output(0, []) == 3
+
+    def test_constant_negative(self):
+        test_operation = Constant(-3)
+        assert test_operation.evaluate_output(0, []) == -3
+
+    def test_constant_complex(self):
+        test_operation = Constant(3+4j)
+        assert test_operation.evaluate_output(0, []) == 3+4j
+
+
+class TestAddition:
+    def test_addition_positive(self):
+        test_operation = Addition()
+        assert test_operation.evaluate_output(0, [3, 5]) == 8
+
+    def test_addition_negative(self):
+        test_operation = Addition()
+        assert test_operation.evaluate_output(0, [-3, -5]) == -8
+
+    def test_addition_complex(self):
+        test_operation = Addition()
+        assert test_operation.evaluate_output(0, [3+5j, 4+6j]) == 7+11j
+
+
+class TestSubtraction:
+    def test_subtraction_positive(self):
+        test_operation = Subtraction()
+        assert test_operation.evaluate_output(0, [5, 3]) == 2
+
+    def test_subtraction_negative(self):
+        test_operation = Subtraction()
+        assert test_operation.evaluate_output(0, [-5, -3]) == -2
+
+    def test_subtraction_complex(self):
+        test_operation = Subtraction()
+        assert test_operation.evaluate_output(0, [3+5j, 4+6j]) == -1-1j
+
+
+class TestMultiplication:
+    def test_multiplication_positive(self):
+        test_operation = Multiplication()
+        assert test_operation.evaluate_output(0, [5, 3]) == 15
+
+    def test_multiplication_negative(self):
+        test_operation = Multiplication()
+        assert test_operation.evaluate_output(0, [-5, -3]) == 15
+
+    def test_multiplication_complex(self):
+        test_operation = Multiplication()
+        assert test_operation.evaluate_output(0, [3+5j, 4+6j]) == -18+38j
+
+
+class TestDivision:
+    def test_division_positive(self):
+        test_operation = Division()
+        assert test_operation.evaluate_output(0, [30, 5]) == 6
+
+    def test_division_negative(self):
+        test_operation = Division()
+        assert test_operation.evaluate_output(0, [-30, -5]) == 6
+
+    def test_division_complex(self):
+        test_operation = Division()
+        assert test_operation.evaluate_output(0, [60+40j, 10+20j]) == 2.8-1.6j
+
+
+class TestSquareRoot:
+    def test_squareroot_positive(self):
+        test_operation = SquareRoot()
+        assert test_operation.evaluate_output(0, [36]) == 6
+
+    def test_squareroot_negative(self):
+        test_operation = SquareRoot()
+        assert test_operation.evaluate_output(0, [-36]) == 6j
+
+    def test_squareroot_complex(self):
+        test_operation = SquareRoot()
+        assert test_operation.evaluate_output(0, [48+64j]) == 8+4j
+
+
+class TestComplexConjugate:
+    def test_complexconjugate_positive(self):
+        test_operation = ComplexConjugate()
+        assert test_operation.evaluate_output(0, [3+4j]) == 3-4j
+
+    def test_test_complexconjugate_negative(self):
+        test_operation = ComplexConjugate()
+        assert test_operation.evaluate_output(0, [-3-4j]) == -3+4j
+
+
+class TestMax:
+    def test_max_positive(self):
+        test_operation = Max()
+        assert test_operation.evaluate_output(0, [30, 5]) == 30
+
+    def test_max_negative(self):
+        test_operation = Max()
+        assert test_operation.evaluate_output(0, [-30, -5]) == -5
+
+
+class TestMin:
+    def test_min_positive(self):
+        test_operation = Min()
+        assert test_operation.evaluate_output(0, [30, 5]) == 5
+
+    def test_min_negative(self):
+        test_operation = Min()
+        assert test_operation.evaluate_output(0, [-30, -5]) == -30
+
+
+class TestAbsolute:
+    def test_absolute_positive(self):
+        test_operation = Absolute()
+        assert test_operation.evaluate_output(0, [30]) == 30
+
+    def test_absolute_negative(self):
+        test_operation = Absolute()
+        assert test_operation.evaluate_output(0, [-5]) == 5
+
+    def test_absolute_complex(self):
+        test_operation = Absolute()
+        assert test_operation.evaluate_output(0, [3+4j]) == 5.0
+
+
+class TestConstantMultiplication:
+    def test_constantmultiplication_positive(self):
+        test_operation = ConstantMultiplication(5)
+        assert test_operation.evaluate_output(0, [20]) == 100
+
+    def test_constantmultiplication_negative(self):
+        test_operation = ConstantMultiplication(5)
+        assert test_operation.evaluate_output(0, [-5]) == -25
+
+    def test_constantmultiplication_complex(self):
+        test_operation = ConstantMultiplication(3+2j)
+        assert test_operation.evaluate_output(0, [3+4j]) == 1+18j
+
+
+class TestButterfly:
+    def test_butterfly_positive(self):
+        test_operation = Butterfly()
+        assert test_operation.evaluate_output(0, [2, 3]) == 5
+        assert test_operation.evaluate_output(1, [2, 3]) == -1
+
+    def test_butterfly_negative(self):
+        test_operation = Butterfly()
+        assert test_operation.evaluate_output(0, [-2, -3]) == -5
+        assert test_operation.evaluate_output(1, [-2, -3]) == 1
+
+    def test_buttefly_complex(self):
+        test_operation = Butterfly()
+        assert test_operation.evaluate_output(0, [2+1j, 3-2j]) == 5-1j
+        assert test_operation.evaluate_output(1, [2+1j, 3-2j]) == -1+3j
+
+
+class TestDepends:
+    def test_depends_addition(self):
+        add1 = Addition()
+        assert set(add1.inputs_required_for_output(0)) == {0, 1}
+
+    def test_depends_butterfly(self):
+        bfly1 = Butterfly()
+        assert set(bfly1.inputs_required_for_output(0)) == {0, 1}
+        assert set(bfly1.inputs_required_for_output(1)) == {0, 1}
diff --git a/test/test_fast_simulation.py b/test/test_fast_simulation.py
new file mode 100644
index 0000000000000000000000000000000000000000..acdea48d2bf51040b59fda5bd99d67ed158a1035
--- /dev/null
+++ b/test/test_fast_simulation.py
@@ -0,0 +1,232 @@
+import pytest
+import numpy as np
+
+from b_asic import SFG, Output, FastSimulation, Addition, Subtraction, Constant, Butterfly
+
+
+class TestRunFor:
+    def test_with_lambdas_as_input(self, sfg_two_inputs_two_outputs):
+        simulation = FastSimulation(sfg_two_inputs_two_outputs, [lambda n: n + 3, lambda n: 1 + n * 2])
+
+        output = simulation.run_for(101, save_results = True)
+
+        assert output[0] == 304
+        assert output[1] == 505
+
+        assert simulation.results["0"][100] == 304
+        assert simulation.results["1"][100] == 505
+
+        assert simulation.results["in1"][0] == 3
+        assert simulation.results["in2"][0] == 1
+        assert simulation.results["add1"][0] == 4
+        assert simulation.results["add2"][0] == 5
+        assert simulation.results["0"][0] == 4
+        assert simulation.results["1"][0] == 5
+
+        assert simulation.results["in1"][1] == 4
+        assert simulation.results["in2"][1] == 3
+        assert simulation.results["add1"][1] == 7
+        assert simulation.results["add2"][1] == 10
+        assert simulation.results["0"][1] == 7
+        assert simulation.results["1"][1] == 10
+
+        assert simulation.results["in1"][2] == 5
+        assert simulation.results["in2"][2] == 5
+        assert simulation.results["add1"][2] == 10
+        assert simulation.results["add2"][2] == 15
+        assert simulation.results["0"][2] == 10
+        assert simulation.results["1"][2] == 15
+
+        assert simulation.results["in1"][3] == 6
+        assert simulation.results["in2"][3] == 7
+        assert simulation.results["add1"][3] == 13
+        assert simulation.results["add2"][3] == 20
+        assert simulation.results["0"][3] == 13
+        assert simulation.results["1"][3] == 20
+
+    def test_with_numpy_arrays_as_input(self, sfg_two_inputs_two_outputs):
+        input0 = np.array([5, 9, 25, -5, 7])
+        input1 = np.array([7, 3, 3,  54, 2])
+        simulation = FastSimulation(sfg_two_inputs_two_outputs, [input0, input1])
+
+        output = simulation.run_for(5, save_results = True)
+
+        assert output[0] == 9
+        assert output[1] == 11
+
+        assert isinstance(simulation.results["in1"], np.ndarray)
+        assert isinstance(simulation.results["in2"], np.ndarray)
+        assert isinstance(simulation.results["add1"], np.ndarray)
+        assert isinstance(simulation.results["add2"], np.ndarray)
+        assert isinstance(simulation.results["0"], np.ndarray)
+        assert isinstance(simulation.results["1"], np.ndarray)
+
+        assert simulation.results["in1"][0] == 5
+        assert simulation.results["in2"][0] == 7
+        assert simulation.results["add1"][0] == 12
+        assert simulation.results["add2"][0] == 19
+        assert simulation.results["0"][0] == 12
+        assert simulation.results["1"][0] == 19
+
+        assert simulation.results["in1"][1] == 9
+        assert simulation.results["in2"][1] == 3
+        assert simulation.results["add1"][1] == 12
+        assert simulation.results["add2"][1] == 15
+        assert simulation.results["0"][1] == 12
+        assert simulation.results["1"][1] == 15
+
+        assert simulation.results["in1"][2] == 25
+        assert simulation.results["in2"][2] == 3
+        assert simulation.results["add1"][2] == 28
+        assert simulation.results["add2"][2] == 31
+        assert simulation.results["0"][2] == 28
+        assert simulation.results["1"][2] == 31
+
+        assert simulation.results["in1"][3] == -5
+        assert simulation.results["in2"][3] == 54
+        assert simulation.results["add1"][3] == 49
+        assert simulation.results["add2"][3] == 103
+        assert simulation.results["0"][3] == 49
+        assert simulation.results["1"][3] == 103
+
+        assert simulation.results["0"][4] == 9
+        assert simulation.results["1"][4] == 11
+    
+    def test_with_numpy_array_overflow(self, sfg_two_inputs_two_outputs):
+        input0 = np.array([5, 9, 25, -5, 7])
+        input1 = np.array([7, 3, 3,  54, 2])
+        simulation = FastSimulation(sfg_two_inputs_two_outputs, [input0, input1])
+        simulation.run_for(5)
+        with pytest.raises(IndexError):
+            simulation.step()
+
+    def test_run_whole_numpy_array(self, sfg_two_inputs_two_outputs):
+        input0 = np.array([5, 9, 25, -5, 7])
+        input1 = np.array([7, 3, 3,  54, 2])
+        simulation = FastSimulation(sfg_two_inputs_two_outputs, [input0, input1])
+        simulation.run()
+        assert len(simulation.results["0"]) == 5
+        assert len(simulation.results["1"]) == 5
+        with pytest.raises(IndexError):
+            simulation.step()
+
+    def test_delay(self, sfg_delay):
+        simulation = FastSimulation(sfg_delay)
+        simulation.set_input(0, [5, -2, 25, -6, 7, 0])
+        simulation.run_for(6, save_results = True)
+
+        assert simulation.results["0"][0] == 0
+        assert simulation.results["0"][1] == 5
+        assert simulation.results["0"][2] == -2
+        assert simulation.results["0"][3] == 25
+        assert simulation.results["0"][4] == -6
+        assert simulation.results["0"][5] == 7
+
+    def test_find_result_key(self, precedence_sfg_delays):
+        sim = FastSimulation(precedence_sfg_delays, [[0, 4, 542, 42, 31.314, 534.123, -453415, 5431]])
+        sim.run()
+        assert sim.results[precedence_sfg_delays.find_result_keys_by_name("ADD2")[0]][4] == 31220
+        assert sim.results[precedence_sfg_delays.find_result_keys_by_name("A1")[0]][2] == 80
+
+class TestRun:
+    def test_save_results(self, sfg_two_inputs_two_outputs):
+        simulation = FastSimulation(sfg_two_inputs_two_outputs, [2, 3])
+        assert not simulation.results
+        simulation.run_for(10, save_results = False)
+        assert not simulation.results
+        simulation.run_for(10)
+        assert len(simulation.results["0"]) == 10
+        assert len(simulation.results["1"]) == 10
+        simulation.run_for(10, save_results = True)
+        assert len(simulation.results["0"]) == 20
+        assert len(simulation.results["1"]) == 20
+        simulation.run_for(10, save_results = False)
+        assert len(simulation.results["0"]) == 20
+        assert len(simulation.results["1"]) == 20
+        simulation.run_for(13, save_results = True)
+        assert len(simulation.results["0"]) == 33
+        assert len(simulation.results["1"]) == 33
+        simulation.step(save_results = False)
+        assert len(simulation.results["0"]) == 33
+        assert len(simulation.results["1"]) == 33
+        simulation.step()
+        assert len(simulation.results["0"]) == 34
+        assert len(simulation.results["1"]) == 34
+        simulation.clear_results()
+        assert not simulation.results
+
+    def test_nested(self, sfg_nested):
+        input0 = np.array([5, 9])
+        input1 = np.array([7, 3])
+        simulation = FastSimulation(sfg_nested, [input0, input1])
+
+        output0 = simulation.step()
+        output1 = simulation.step()
+
+        assert output0[0] == 11405
+        assert output1[0] == 4221
+    
+    def test_accumulator(self, sfg_accumulator):
+        data_in = np.array([5, -2, 25, -6, 7, 0])
+        reset   = np.array([0, 0,  0,  1,  0, 0])
+        simulation = FastSimulation(sfg_accumulator, [data_in, reset])
+        output0 = simulation.step()
+        output1 = simulation.step()
+        output2 = simulation.step()
+        output3 = simulation.step()
+        output4 = simulation.step()
+        output5 = simulation.step()
+        assert output0[0] == 0
+        assert output1[0] == 5
+        assert output2[0] == 3
+        assert output3[0] == 28
+        assert output4[0] == 0
+        assert output5[0] == 7
+
+    def test_simple_accumulator(self, sfg_simple_accumulator):
+        data_in = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+        simulation = FastSimulation(sfg_simple_accumulator, [data_in])
+        simulation.run()
+        assert list(simulation.results["0"]) == [0, 1, 3, 6, 10, 15, 21, 28, 36, 45]
+        
+    def test_simple_filter(self, sfg_simple_filter):
+        input0 = np.array([1, 2, 3, 4, 5])
+        simulation = FastSimulation(sfg_simple_filter, [input0])
+        simulation.run_for(len(input0), save_results = True)
+        assert all(simulation.results["0"] == np.array([0, 1.0, 2.5, 4.25, 6.125]))
+
+    def test_custom_operation(self, sfg_custom_operation):
+        simulation = FastSimulation(sfg_custom_operation, [lambda n: n + 1])
+        simulation.run_for(5)
+        assert all(simulation.results["0"] == np.array([2, 4, 6, 8, 10]))
+        assert all(simulation.results["1"] == np.array([2, 4, 8, 16, 32]))
+
+
+class TestLarge:
+    def test_1k_additions(self):
+        prev_op = Addition(Constant(1), Constant(1))
+        for _ in range(999):
+            prev_op = Addition(prev_op, Constant(2))
+        sfg = SFG(outputs=[Output(prev_op)])
+        simulation = FastSimulation(sfg, [])
+        assert simulation.step()[0] == 2000
+        
+    def test_1k_subtractions(self):
+        prev_op = Subtraction(Constant(0), Constant(2))
+        for _ in range(999):
+            prev_op = Subtraction(prev_op, Constant(2))
+        sfg = SFG(outputs=[Output(prev_op)])
+        simulation = FastSimulation(sfg, [])
+        assert simulation.step()[0] == -2000
+        
+    def test_1k_butterfly(self):
+        prev_op_add = Addition(Constant(1), Constant(1))
+        prev_op_sub = Subtraction(Constant(-1), Constant(1))
+        for _ in range(499):
+            prev_op_add = Addition(prev_op_add, Constant(2))
+        for _ in range(499):
+            prev_op_sub = Subtraction(prev_op_sub, Constant(2))
+        butterfly = Butterfly(prev_op_add, prev_op_sub)
+        sfg = SFG(outputs=[Output(butterfly.output(0)), Output(butterfly.output(1))])
+        simulation = FastSimulation(sfg, [])
+        assert list(simulation.step()) == [0, 2000]
\ No newline at end of file
diff --git a/test/test_graph_id_generator.py b/test/test_graph_id_generator.py
index b14597eabe6c15695c5c452f69f3deeab56e36d5..72c923b63b6af74296cca86cd432da7e488d55b6 100644
--- a/test/test_graph_id_generator.py
+++ b/test/test_graph_id_generator.py
@@ -2,9 +2,10 @@
 B-ASIC test suite for graph id generator.
 """
 
-from b_asic.graph_id import GraphIDGenerator, GraphID
 import pytest
 
+from b_asic import GraphIDGenerator, GraphID
+
 @pytest.fixture
 def graph_id_generator():
     return GraphIDGenerator()
@@ -12,17 +13,17 @@ def graph_id_generator():
 class TestGetNextId:
     def test_empty_string_generator(self, graph_id_generator):
         """Test the graph id generator for an empty string type."""
-        assert graph_id_generator.get_next_id("") == "1"
-        assert graph_id_generator.get_next_id("") == "2"
+        assert graph_id_generator.next_id("") == "1"
+        assert graph_id_generator.next_id("") == "2"
 
     def test_normal_string_generator(self, graph_id_generator):
         """"Test the graph id generator for a normal string type."""
-        assert graph_id_generator.get_next_id("add") == "add1"
-        assert graph_id_generator.get_next_id("add") == "add2"
+        assert graph_id_generator.next_id("add") == "add1"
+        assert graph_id_generator.next_id("add") == "add2"
 
     def test_different_strings_generator(self, graph_id_generator):
         """Test the graph id generator for different strings."""
-        assert graph_id_generator.get_next_id("sub") == "sub1"
-        assert graph_id_generator.get_next_id("mul") == "mul1"
-        assert graph_id_generator.get_next_id("sub") == "sub2"
-        assert graph_id_generator.get_next_id("mul") == "mul2"
+        assert graph_id_generator.next_id("sub") == "sub1"
+        assert graph_id_generator.next_id("mul") == "mul1"
+        assert graph_id_generator.next_id("sub") == "sub2"
+        assert graph_id_generator.next_id("mul") == "mul2"
diff --git a/test/test_inputport.py b/test/test_inputport.py
index a43240693ac632b48461023536ff46b0ea379c5c..f4668938ce3aa90e052115d57e3d3d52c9d9e3eb 100644
--- a/test/test_inputport.py
+++ b/test/test_inputport.py
@@ -4,92 +4,64 @@ B-ASIC test suite for Inputport
 
 import pytest
 
-from b_asic import InputPort, OutputPort
-from b_asic import Signal
+from b_asic import InputPort, OutputPort, Signal
 
 @pytest.fixture
-def inp_port():
-    return InputPort(0, None)
-
-@pytest.fixture
-def out_port():
-    return OutputPort(0, None)
-
-@pytest.fixture
-def out_port2():
-    return OutputPort(1, None)
+def output_port2():
+    return OutputPort(None, 1)
 
 @pytest.fixture
 def dangling_sig():
     return Signal()
 
 @pytest.fixture
-def s_w_source():
-    out_port = OutputPort(0, None)
-    return Signal(source=out_port)
+def s_w_source(output_port):
+    return Signal(source=output_port)
 
 @pytest.fixture
-def sig_with_dest():
-    inp_port = InputPort(0, None)
-    return Signal(destination=out_port)
+def sig_with_dest(inp_port):
+    return Signal(destination=inp_port)
 
 @pytest.fixture
-def connected_sig():
-    out_port = OutputPort(0, None)
-    inp_port = InputPort(0, None)
-    return Signal(source=out_port, destination=inp_port)
+def connected_sig(inp_port, output_port):
+    return Signal(source=output_port, destination=inp_port)
 
-def test_connect_then_disconnect(inp_port, out_port):
+def test_connect_then_disconnect(input_port, output_port):
     """Test connect unused port to port."""
-    s1 = inp_port.connect(out_port)
-
-    assert inp_port.connected_ports == [out_port]
-    assert out_port.connected_ports == [inp_port]
-    assert inp_port.signals == [s1]
-    assert out_port.signals == [s1]
-    assert s1.source is out_port
-    assert s1.destination is inp_port
-
-    inp_port.remove_signal(s1)
-
-    assert inp_port.connected_ports == []
-    assert out_port.connected_ports == []
-    assert inp_port.signals == []
-    assert out_port.signals == [s1]
-    assert s1.source is out_port
+    s1 = input_port.connect(output_port)
+
+    assert input_port.connected_source == output_port
+    assert input_port.signals == [s1]
+    assert output_port.signals == [s1]
+    assert s1.source is output_port
+    assert s1.destination is input_port
+
+    input_port.remove_signal(s1)
+
+    assert input_port.connected_source is None
+    assert input_port.signals == []
+    assert output_port.signals == [s1]
+    assert s1.source is output_port
     assert s1.destination is None
 
-def test_connect_used_port_to_new_port(inp_port, out_port, out_port2):
-    """Does connecting multiple ports to an inputport throw error?"""
-    inp_port.connect(out_port)
-    with pytest.raises(AssertionError):
-        inp_port.connect(out_port2)
+def test_connect_used_port_to_new_port(input_port, output_port, output_port2):
+    """Multiple connections to an input port should throw an error."""
+    input_port.connect(output_port)
+    with pytest.raises(Exception):
+        input_port.connect(output_port2)
 
-def test_add_signal_then_disconnect(inp_port, s_w_source):
+def test_add_signal_then_disconnect(input_port, s_w_source):
     """Can signal be connected then disconnected properly?"""
-    inp_port.add_signal(s_w_source)
+    input_port.add_signal(s_w_source)
 
-    assert inp_port.connected_ports == [s_w_source.source]
-    assert s_w_source.source.connected_ports == [inp_port]
-    assert inp_port.signals == [s_w_source]
+    assert input_port.connected_source == s_w_source.source
+    assert input_port.signals == [s_w_source]
     assert s_w_source.source.signals == [s_w_source]
-    assert s_w_source.destination is inp_port
+    assert s_w_source.destination is input_port
 
-    inp_port.remove_signal(s_w_source)
+    input_port.remove_signal(s_w_source)
 
-    assert inp_port.connected_ports == []
-    assert s_w_source.source.connected_ports == []
-    assert inp_port.signals == []
+    assert input_port.connected_source is None
+    assert input_port.signals == []
     assert s_w_source.source.signals == [s_w_source]
     assert s_w_source.destination is None
-
-def test_connect_then_disconnect(inp_port, out_port):
-    """Can port be connected and then disconnected properly?"""
-    inp_port.connect(out_port)
-
-    inp_port.disconnect(out_port)
-
-    print("outport signals:", out_port.signals, "count:", out_port.signal_count())
-    assert inp_port.signal_count() == 1
-    assert len(inp_port.connected_ports) == 0
-    assert out_port.signal_count() == 0
diff --git a/test/test_operation.py b/test/test_operation.py
index 6c37e30bddd0b55ea69ae5b95a341c1ddeb56847..f4af81b57b8d30fe71025ab82f75f57f195ce350 100644
--- a/test/test_operation.py
+++ b/test/test_operation.py
@@ -1,9 +1,95 @@
-from b_asic.core_operations import Constant, Addition
-from b_asic.signal import Signal
-from b_asic.port import InputPort, OutputPort
+"""
+B-ASIC test suite for the AbstractOperation class.
+"""
 
 import pytest
 
+from b_asic import Addition, Subtraction, Multiplication, ConstantMultiplication, Division, Constant, Butterfly, \
+    MAD, SquareRoot
+
+
+class TestOperationOverloading:
+    def test_addition_overload(self):
+        """Tests addition overloading for both operation and number argument."""
+        add1 = Addition(None, None, "add1")
+        add2 = Addition(None, None, "add2")
+
+        add3 = add1 + add2
+        assert isinstance(add3, Addition)
+        assert add3.input(0).signals == add1.output(0).signals
+        assert add3.input(1).signals == add2.output(0).signals
+
+        add4 = add3 + 5
+        assert isinstance(add4, Addition)
+        assert add4.input(0).signals == add3.output(0).signals
+        assert add4.input(1).signals[0].source.operation.value == 5
+
+        add5 = 5 + add4
+        assert isinstance(add5, Addition)
+        assert add5.input(0).signals[0].source.operation.value == 5
+        assert add5.input(1).signals == add4.output(0).signals
+
+    def test_subtraction_overload(self):
+        """Tests subtraction overloading for both operation and number argument."""
+        add1 = Addition(None, None, "add1")
+        add2 = Addition(None, None, "add2")
+
+        sub1 = add1 - add2
+        assert isinstance(sub1, Subtraction)
+        assert sub1.input(0).signals == add1.output(0).signals
+        assert sub1.input(1).signals == add2.output(0).signals
+
+        sub2 = sub1 - 5
+        assert isinstance(sub2, Subtraction)
+        assert sub2.input(0).signals == sub1.output(0).signals
+        assert sub2.input(1).signals[0].source.operation.value == 5
+
+        sub3 = 5 - sub2
+        assert isinstance(sub3, Subtraction)
+        assert sub3.input(0).signals[0].source.operation.value == 5
+        assert sub3.input(1).signals == sub2.output(0).signals
+
+    def test_multiplication_overload(self):
+        """Tests multiplication overloading for both operation and number argument."""
+        add1 = Addition(None, None, "add1")
+        add2 = Addition(None, None, "add2")
+
+        mul1 = add1 * add2
+        assert isinstance(mul1, Multiplication)
+        assert mul1.input(0).signals == add1.output(0).signals
+        assert mul1.input(1).signals == add2.output(0).signals
+
+        mul2 = mul1 * 5
+        assert isinstance(mul2, ConstantMultiplication)
+        assert mul2.input(0).signals == mul1.output(0).signals
+        assert mul2.value == 5
+
+        mul3 = 5 * mul2
+        assert isinstance(mul3, ConstantMultiplication)
+        assert mul3.input(0).signals == mul2.output(0).signals
+        assert mul3.value == 5
+
+    def test_division_overload(self):
+        """Tests division overloading for both operation and number argument."""
+        add1 = Addition(None, None, "add1")
+        add2 = Addition(None, None, "add2")
+
+        div1 = add1 / add2
+        assert isinstance(div1, Division)
+        assert div1.input(0).signals == add1.output(0).signals
+        assert div1.input(1).signals == add2.output(0).signals
+
+        div2 = div1 / 5
+        assert isinstance(div2, Division)
+        assert div2.input(0).signals == div1.output(0).signals
+        assert div2.input(1).signals[0].source.operation.value == 5
+
+        div3 = 5 / div2
+        assert isinstance(div3, Division)
+        assert div3.input(0).signals[0].source.operation.value == 5
+        assert div3.input(1).signals == div2.output(0).signals
+
+
 class TestTraverse:
     def test_traverse_single_tree(self, operation):
         """Traverse a tree consisting of one operation."""
@@ -12,20 +98,89 @@ class TestTraverse:
 
     def test_traverse_tree(self, operation_tree):
         """Traverse a basic addition tree with two constants."""
-        assert len(list(operation_tree.traverse())) == 3
+        assert len(list(operation_tree.traverse())) == 5
 
     def test_traverse_large_tree(self, large_operation_tree):
         """Traverse a larger tree."""
-        assert len(list(large_operation_tree.traverse())) == 7
+        assert len(list(large_operation_tree.traverse())) == 13
 
     def test_traverse_type(self, large_operation_tree):
-        traverse = list(large_operation_tree.traverse())
-        assert len(list(filter(lambda type_: isinstance(type_, Addition), traverse))) == 3
-        assert len(list(filter(lambda type_: isinstance(type_, Constant), traverse))) == 4
-
-    def test_traverse_loop(self, operation_tree):
-        add_oper_signal = Signal()
-        operation_tree._output_ports[0].add_signal(add_oper_signal)
-        operation_tree._input_ports[0].remove_signal(add_oper_signal)
-        operation_tree._input_ports[0].add_signal(add_oper_signal)
-        assert len(list(operation_tree.traverse())) == 2
+        result = list(large_operation_tree.traverse())
+        assert len(list(filter(lambda type_: isinstance(type_, Addition), result))) == 3
+        assert len(list(filter(lambda type_: isinstance(type_, Constant), result))) == 4
+
+    def test_traverse_loop(self, operation_graph_with_cycle):
+        assert len(list(operation_graph_with_cycle.traverse())) == 8
+
+
+class TestToSfg:
+    def test_convert_mad_to_sfg(self):
+        mad1 = MAD()
+        mad1_sfg = mad1.to_sfg()
+
+        assert mad1.evaluate(1, 1, 1) == mad1_sfg.evaluate(1, 1, 1)
+        assert len(mad1_sfg.operations) == 6
+
+    def test_butterfly_to_sfg(self):
+        but1 = Butterfly()
+        but1_sfg = but1.to_sfg()
+
+        assert but1.evaluate(1, 1)[0] == but1_sfg.evaluate(1, 1)[0]
+        assert but1.evaluate(1, 1)[1] == but1_sfg.evaluate(1, 1)[1]
+        assert len(but1_sfg.operations) == 8
+
+    def test_add_to_sfg(self):
+        add1 = Addition()
+        add1_sfg = add1.to_sfg()
+
+        assert len(add1_sfg.operations) == 4
+
+    def test_sqrt_to_sfg(self):
+        sqrt1 = SquareRoot()
+        sqrt1_sfg = sqrt1.to_sfg()
+
+        assert len(sqrt1_sfg.operations) == 3
+
+
+class TestLatency:
+    def test_latency_constructor(self):
+        bfly = Butterfly(latency=5)
+
+        assert bfly.latency == 5
+        assert bfly.latency_offsets == {'in0': 0, 'in1': 0, 'out0': 5, 'out1': 5}
+
+    def test_latency_offsets_constructor(self):
+        bfly = Butterfly(latency_offsets={'in0': 2, 'in1': 3, 'out0': 5, 'out1': 10})
+
+        assert bfly.latency == 8
+        assert bfly.latency_offsets == {'in0': 2, 'in1': 3, 'out0': 5, 'out1': 10}
+
+    def test_latency_and_latency_offsets_constructor(self):
+        bfly = Butterfly(latency=5, latency_offsets={'in1': 2, 'out0': 9})
+
+        assert bfly.latency == 9
+        assert bfly.latency_offsets == {"in0": 0, "in1": 2, "out0": 9, "out1": 5}
+
+    def test_set_latency(self):
+        bfly = Butterfly()
+
+        bfly.set_latency(9)
+
+        assert bfly.latency == 9
+        assert bfly.latency_offsets == {"in0": 0, "in1": 0, "out0": 9, "out1": 9}
+
+    def test_set_latency_offsets(self):
+        bfly = Butterfly()
+
+        bfly.set_latency_offsets({'in0': 3, 'out1': 5})
+
+        assert bfly.latency_offsets == {'in0': 3, "in1": None, "out0": None, 'out1': 5}
+
+
+class TestCopyOperation:
+    def test_copy_buttefly_latency_offsets(self):
+        bfly = Butterfly(latency_offsets={'in0': 4, 'in1': 2, 'out0': 10, 'out1': 9})
+
+        bfly_copy = bfly.copy_component()
+
+        assert bfly_copy.latency_offsets == {'in0': 4, 'in1': 2, 'out0': 10, 'out1': 9}
diff --git a/test/test_outputport.py b/test/test_outputport.py
index deed7a1e06836600254e3903b8b45a3d05f17cbe..7cc250ee083bdecb5ee04c3c28e56518a5bb6331 100644
--- a/test/test_outputport.py
+++ b/test/test_outputport.py
@@ -1,80 +1,70 @@
 """
 B-ASIC test suite for OutputPort.
 """
-from b_asic import OutputPort, InputPort, Signal
 import pytest
 
-@pytest.fixture
-def output_port():
-    return OutputPort(0, None)
-
-@pytest.fixture
-def input_port():
-    return InputPort(0, None)
-
-@pytest.fixture
-def list_of_input_ports():
-    return [InputPort(_, None) for _ in range(0,3)]
+from b_asic import OutputPort, InputPort, Signal
 
 class TestConnect:
     def test_multiple_ports(self, output_port, list_of_input_ports):
-        """Can multiple ports connect to an output port?"""
+        """Multiple connections to an output port should be possible."""
         for port in list_of_input_ports:
-            output_port.connect(port)
+            port.connect(output_port)
 
-        assert output_port.signal_count() == len(list_of_input_ports)
+        assert output_port.signal_count == len(list_of_input_ports)
 
-    def test_same_port(self, output_port, list_of_input_ports):
+    def test_same_port(self, output_port, input_port):
         """Check error handing."""
-        output_port.connect(list_of_input_ports[0])
-        with pytest.raises(AssertionError):
-            output_port.connect(list_of_input_ports[0])
+        input_port.connect(output_port)
+        with pytest.raises(Exception):
+            input_port.connect(output_port)
 
-        assert output_port.signal_count() == 2
+        assert output_port.signal_count == 1
 
 class TestAddSignal:
     def test_dangling(self, output_port):
         s = Signal()
         output_port.add_signal(s)
 
-        assert output_port.signal_count() == 1
-
-    def test_with_destination(self, output_port, input_port):
-        s = Signal(destination=input_port)
-        output_port.add_signal(s)
+        assert output_port.signal_count == 1
+        assert output_port.signals == [s]
 
-        assert output_port.connected_ports == [s.destination]
+class TestClear:
+    def test_others_clear(self, output_port, list_of_input_ports):
+        for port in list_of_input_ports:
+            port.connect(output_port)
 
-class TestDisconnect:
-    def test_multiple_ports(self, output_port, list_of_input_ports):
-        """Can multiple ports disconnect from OutputPort?"""
         for port in list_of_input_ports:
-            output_port.connect(port)
+            port.clear()
+
+        assert output_port.signal_count == 3
+        assert all(s.dangling() for s in output_port.signals)
 
+    def test_self_clear(self, output_port, list_of_input_ports):
         for port in list_of_input_ports:
-            output_port.disconnect(port)
+            port.connect(output_port)
 
-        assert output_port.signal_count() == 3
-        assert output_port.connected_ports == []
+        output_port.clear()
+
+        assert output_port.signal_count == 0
+        assert output_port.signals == []
 
 class TestRemoveSignal:
     def test_one_signal(self, output_port, input_port):
-        s = output_port.connect(input_port)
+        s = input_port.connect(output_port)
         output_port.remove_signal(s)
 
-        assert output_port.signal_count() == 0
+        assert output_port.signal_count == 0
         assert output_port.signals == []
-        assert output_port.connected_ports == []
 
     def test_multiple_signals(self, output_port, list_of_input_ports):
-        """Can multiple signals disconnect from OutputPort?"""
         sigs = []
 
         for port in list_of_input_ports:
-            sigs.append(output_port.connect(port))
+            sigs.append(port.connect(output_port))
 
-        for sig in sigs:
-            output_port.remove_signal(sig)
+        for s in sigs:
+            output_port.remove_signal(s)
 
-        assert output_port.signal_count() == 0
+        assert output_port.signal_count == 0
         assert output_port.signals == []
diff --git a/test/test_schema.py b/test/test_schema.py
new file mode 100644
index 0000000000000000000000000000000000000000..78a713a9ceda574feede72f48bacf02e6a9c4025
--- /dev/null
+++ b/test/test_schema.py
@@ -0,0 +1,67 @@
+"""
+B-ASIC test suite for the schema module and Schema class.
+"""
+
+from b_asic import Schema, Addition, ConstantMultiplication
+
+
+class TestInit:
+    def test_simple_filter_normal_latency(self, sfg_simple_filter):
+        sfg_simple_filter.set_latency_of_type(Addition.type_name(), 5)
+        sfg_simple_filter.set_latency_of_type(ConstantMultiplication.type_name(), 4)
+
+        schema = Schema(sfg_simple_filter)
+
+        assert schema._start_times == {"add1": 4, "cmul1": 0}
+
+    def test_complicated_single_outputs_normal_latency(self, precedence_sfg_delays):
+        precedence_sfg_delays.set_latency_of_type(Addition.type_name(), 4)
+        precedence_sfg_delays.set_latency_of_type(ConstantMultiplication.type_name(), 3)
+
+        schema = Schema(precedence_sfg_delays, scheduling_alg="ASAP")
+
+        for op in schema._sfg.get_operations_topological_order():
+            print(op.latency_offsets)
+
+        start_times_names = dict()
+        for op_id, start_time in schema._start_times.items():
+            op_name = precedence_sfg_delays.find_by_id(op_id).name
+            start_times_names[op_name] = start_time
+
+        assert start_times_names == {"C0": 0, "B1": 0, "B2": 0, "ADD2": 3, "ADD1": 7, "Q1": 11,
+                                     "A0": 14, "A1": 0, "A2": 0, "ADD3": 3, "ADD4": 17}
+
+    def test_complicated_single_outputs_complex_latencies(self, precedence_sfg_delays):
+        precedence_sfg_delays.set_latency_offsets_of_type(ConstantMultiplication.type_name(), {'in0': 3, 'out0': 5})
+
+        precedence_sfg_delays.find_by_name("B1")[0].set_latency_offsets({'in0': 4, 'out0': 7})
+        precedence_sfg_delays.find_by_name("B2")[0].set_latency_offsets({'in0': 1, 'out0': 4})
+        precedence_sfg_delays.find_by_name("ADD2")[0].set_latency_offsets({'in0': 4, 'in1': 2, 'out0': 4})
+        precedence_sfg_delays.find_by_name("ADD1")[0].set_latency_offsets({'in0': 1, 'in1': 2, 'out0': 4})
+        precedence_sfg_delays.find_by_name("Q1")[0].set_latency_offsets({'in0': 3, 'out0': 6})
+        precedence_sfg_delays.find_by_name("A0")[0].set_latency_offsets({'in0': 0, 'out0': 2})
+
+        precedence_sfg_delays.find_by_name("A1")[0].set_latency_offsets({'in0': 0, 'out0': 5})
+        precedence_sfg_delays.find_by_name("A2")[0].set_latency_offsets({'in0': 2, 'out0': 3})
+        precedence_sfg_delays.find_by_name("ADD3")[0].set_latency_offsets({'in0': 2, 'in1': 1, 'out0': 4})
+        precedence_sfg_delays.find_by_name("ADD4")[0].set_latency_offsets({'in0': 6, 'in1': 7, 'out0': 9})
+
+        schema = Schema(precedence_sfg_delays, scheduling_alg="ASAP")
+
+        start_times_names = dict()
+        for op_id, start_time in schema._start_times.items():
+            op_name = precedence_sfg_delays.find_by_id(op_id).name
+            start_times_names[op_name] = start_time
+
+        assert start_times_names == {'C0': 0, 'B1': 0, 'B2': 0, 'ADD2': 3, 'ADD1': 5, 'Q1': 6, 'A0': 12,
+                                     'A1': 0, 'A2': 0, 'ADD3': 3, 'ADD4': 8}
+
+    def test_independent_sfg(self, sfg_two_inputs_two_outputs_independent_with_cmul):
+        schema = Schema(sfg_two_inputs_two_outputs_independent_with_cmul, scheduling_alg="ASAP")
+
+        start_times_names = dict()
+        for op_id, start_time in schema._start_times.items():
+            op_name = sfg_two_inputs_two_outputs_independent_with_cmul.find_by_id(op_id).name
+            start_times_names[op_name] = start_time
+
+        assert start_times_names == {'CMUL1': 0, 'CMUL2': 5, "ADD1": 0, "CMUL3": 7}
diff --git a/test/test_sfg.py b/test/test_sfg.py
new file mode 100644
index 0000000000000000000000000000000000000000..2124daa9f9694d395292485ed1036efe63c9c088
--- /dev/null
+++ b/test/test_sfg.py
@@ -0,0 +1,1018 @@
+from os import path, remove
+import pytest
+import random
+import string
+import io
+import sys
+
+from b_asic import SFG, Signal, Input, Output, Delay, FastSimulation
+from b_asic.core_operations import Constant, Addition, Subtraction, Multiplication, \
+    Division, Min, Max, SquareRoot, ComplexConjugate, Absolute, ConstantMultiplication, \
+    Butterfly
+
+from b_asic.save_load_structure import sfg_to_python, python_to_sfg
+
+
+class TestInit:
+    def test_direct_input_to_output_sfg_construction(self):
+        in1 = Input("IN1")
+        out1 = Output(None, "OUT1")
+        out1.input(0).connect(in1, "S1")
+
+        sfg = SFG(inputs=[in1], outputs=[out1])  # in1 ---s1---> out1
+
+        assert len(list(sfg.components)) == 3
+        assert len(list(sfg.operations)) == 2
+        assert sfg.input_count == 1
+        assert sfg.output_count == 1
+
+    def test_same_signal_input_and_output_sfg_construction(self):
+        add1 = Addition(None, None, "ADD1")
+        add2 = Addition(None, None, "ADD2")
+
+        s1 = add2.input(0).connect(add1, "S1")
+
+        # in1 ---s1---> out1
+        sfg = SFG(input_signals=[s1], output_signals=[s1])
+
+        assert len(list(sfg.components)) == 3
+        assert len(list(sfg.operations)) == 2
+        assert sfg.input_count == 1
+        assert sfg.output_count == 1
+
+    def test_outputs_construction(self, operation_tree):
+        sfg = SFG(outputs=[Output(operation_tree)])
+
+        assert len(list(sfg.components)) == 7
+        assert len(list(sfg.operations)) == 4
+        assert sfg.input_count == 0
+        assert sfg.output_count == 1
+
+    def test_signals_construction(self, operation_tree):
+        sfg = SFG(output_signals=[Signal(source=operation_tree.output(0))])
+
+        assert len(list(sfg.components)) == 7
+        assert len(list(sfg.operations)) == 4
+        assert sfg.input_count == 0
+        assert sfg.output_count == 1
+
+
+class TestPrintSfg:
+    def test_one_addition(self):
+        inp1 = Input("INP1")
+        inp2 = Input("INP2")
+        add1 = Addition(inp1, inp2, "ADD1")
+        out1 = Output(add1, "OUT1")
+        sfg = SFG(inputs=[inp1, inp2], outputs=[out1], name="SFG1")
+
+        assert sfg.__str__() == \
+            "id: no_id, \tname: SFG1, \tinputs: {0: '-'}, \toutputs: {0: '-'}\n" + \
+            "Internal Operations:\n" + \
+            "----------------------------------------------------------------------------------------------------\n" + \
+            str(sfg.find_by_name("INP1")[0]) + "\n" + \
+            str(sfg.find_by_name("INP2")[0]) + "\n" + \
+            str(sfg.find_by_name("ADD1")[0]) + "\n" + \
+            str(sfg.find_by_name("OUT1")[0]) + "\n" + \
+            "----------------------------------------------------------------------------------------------------\n"
+
+    def test_add_mul(self):
+        inp1 = Input("INP1")
+        inp2 = Input("INP2")
+        inp3 = Input("INP3")
+        add1 = Addition(inp1, inp2, "ADD1")
+        mul1 = Multiplication(add1, inp3, "MUL1")
+        out1 = Output(mul1, "OUT1")
+        sfg = SFG(inputs=[inp1, inp2, inp3], outputs=[out1], name="mac_sfg")
+
+        assert sfg.__str__() == \
+            "id: no_id, \tname: mac_sfg, \tinputs: {0: '-'}, \toutputs: {0: '-'}\n" + \
+            "Internal Operations:\n" + \
+            "----------------------------------------------------------------------------------------------------\n" + \
+            str(sfg.find_by_name("INP1")[0]) + "\n" + \
+            str(sfg.find_by_name("INP2")[0]) + "\n" + \
+            str(sfg.find_by_name("ADD1")[0]) + "\n" + \
+            str(sfg.find_by_name("INP3")[0]) + "\n" + \
+            str(sfg.find_by_name("MUL1")[0]) + "\n" + \
+            str(sfg.find_by_name("OUT1")[0]) + "\n" + \
+            "----------------------------------------------------------------------------------------------------\n"
+
+    def test_constant(self):
+        inp1 = Input("INP1")
+        const1 = Constant(3, "CONST")
+        add1 = Addition(const1, inp1, "ADD1")
+        out1 = Output(add1, "OUT1")
+
+        sfg = SFG(inputs=[inp1], outputs=[out1], name="sfg")
+
+        assert sfg.__str__() == \
+            "id: no_id, \tname: sfg, \tinputs: {0: '-'}, \toutputs: {0: '-'}\n" + \
+            "Internal Operations:\n" + \
+            "----------------------------------------------------------------------------------------------------\n" + \
+            str(sfg.find_by_name("CONST")[0]) + "\n" + \
+            str(sfg.find_by_name("INP1")[0]) + "\n" + \
+            str(sfg.find_by_name("ADD1")[0]) + "\n" + \
+            str(sfg.find_by_name("OUT1")[0]) + "\n" + \
+            "----------------------------------------------------------------------------------------------------\n"
+
+    def test_simple_filter(self, sfg_simple_filter):
+        assert sfg_simple_filter.__str__() == \
+            "id: no_id, \tname: simple_filter, \tinputs: {0: '-'}, \toutputs: {0: '-'}\n" + \
+            "Internal Operations:\n" + \
+            "----------------------------------------------------------------------------------------------------\n" + \
+            str(sfg_simple_filter.find_by_name("IN1")[0]) + "\n" + \
+            str(sfg_simple_filter.find_by_name("ADD1")[0]) + "\n" + \
+            str(sfg_simple_filter.find_by_name("T1")[0]) + "\n" + \
+            str(sfg_simple_filter.find_by_name("CMUL1")[0]) + "\n" + \
+            str(sfg_simple_filter.find_by_name("OUT1")[0]) + "\n" + \
+            "----------------------------------------------------------------------------------------------------\n"
+
+
+class TestDeepCopy:
+    def test_deep_copy_no_duplicates(self):
+        inp1 = Input("INP1")
+        inp2 = Input("INP2")
+        inp3 = Input("INP3")
+        add1 = Addition(inp1, inp2, "ADD1")
+        mul1 = Multiplication(add1, inp3, "MUL1")
+        out1 = Output(mul1, "OUT1")
+
+        mac_sfg = SFG(inputs=[inp1, inp2], outputs=[out1], name="mac_sfg")
+        mac_sfg_new = mac_sfg()
+
+        assert mac_sfg.name == "mac_sfg"
+        assert mac_sfg_new.name == ""
+
+        for g_id, component in mac_sfg._components_by_id.items():
+            component_copy = mac_sfg_new.find_by_id(g_id)
+            assert component.name == component_copy.name
+
+    def test_deep_copy(self):
+        inp1 = Input("INP1")
+        inp2 = Input("INP2")
+        inp3 = Input("INP3")
+        add1 = Addition(None, None, "ADD1")
+        add2 = Addition(None, None, "ADD2")
+        mul1 = Multiplication(None, None, "MUL1")
+        out1 = Output(None, "OUT1")
+
+        add1.input(0).connect(inp1, "S1")
+        add1.input(1).connect(inp2, "S2")
+        add2.input(0).connect(add1, "S4")
+        add2.input(1).connect(inp3, "S3")
+        mul1.input(0).connect(add1, "S5")
+        mul1.input(1).connect(add2, "S6")
+        out1.input(0).connect(mul1, "S7")
+
+        mac_sfg = SFG(inputs=[inp1, inp2], outputs=[out1],
+                      id_number_offset=100, name="mac_sfg")
+        mac_sfg_new = mac_sfg(name="mac_sfg2")
+
+        assert mac_sfg.name == "mac_sfg"
+        assert mac_sfg_new.name == "mac_sfg2"
+        assert mac_sfg.id_number_offset == 100
+        assert mac_sfg_new.id_number_offset == 100
+
+        for g_id, component in mac_sfg._components_by_id.items():
+            component_copy = mac_sfg_new.find_by_id(g_id)
+            assert component.name == component_copy.name
+
+    def test_deep_copy_with_new_sources(self):
+        inp1 = Input("INP1")
+        inp2 = Input("INP2")
+        inp3 = Input("INP3")
+        add1 = Addition(inp1, inp2, "ADD1")
+        mul1 = Multiplication(add1, inp3, "MUL1")
+        out1 = Output(mul1, "OUT1")
+
+        mac_sfg = SFG(inputs=[inp1, inp2], outputs=[out1], name="mac_sfg")
+
+        a = Addition(Constant(3), Constant(5))
+        b = Constant(2)
+        mac_sfg_new = mac_sfg(a, b)
+        assert mac_sfg_new.input(0).signals[0].source.operation is a
+        assert mac_sfg_new.input(1).signals[0].source.operation is b
+
+
+class TestEvaluateOutput:
+    def test_evaluate_output(self, operation_tree):
+        sfg = SFG(outputs=[Output(operation_tree)])
+        assert sfg.evaluate_output(0, []) == 5
+
+    def test_evaluate_output_large(self, large_operation_tree):
+        sfg = SFG(outputs=[Output(large_operation_tree)])
+        assert sfg.evaluate_output(0, []) == 14
+
+    def test_evaluate_output_cycle(self, operation_graph_with_cycle):
+        sfg = SFG(outputs=[Output(operation_graph_with_cycle)])
+        with pytest.raises(Exception):
+            sfg.evaluate_output(0, [])
+
+
+class TestComponents:
+    def test_advanced_components(self):
+        inp1 = Input("INP1")
+        inp2 = Input("INP2")
+        inp3 = Input("INP3")
+        add1 = Addition(None, None, "ADD1")
+        add2 = Addition(None, None, "ADD2")
+        mul1 = Multiplication(None, None, "MUL1")
+        out1 = Output(None, "OUT1")
+
+        add1.input(0).connect(inp1, "S1")
+        add1.input(1).connect(inp2, "S2")
+        add2.input(0).connect(add1, "S4")
+        add2.input(1).connect(inp3, "S3")
+        mul1.input(0).connect(add1, "S5")
+        mul1.input(1).connect(add2, "S6")
+        out1.input(0).connect(mul1, "S7")
+
+        mac_sfg = SFG(inputs=[inp1, inp2], outputs=[out1], name="mac_sfg")
+
+        assert set([comp.name for comp in mac_sfg.components]) == {
+            "INP1", "INP2", "INP3", "ADD1", "ADD2", "MUL1", "OUT1", "S1", "S2", "S3", "S4", "S5", "S6", "S7"}
+
+
+class TestReplaceComponents:
+    def test_replace_addition_by_id(self, operation_tree):
+        sfg = SFG(outputs=[Output(operation_tree)])
+        component_id = "add1"
+
+        sfg = sfg.replace_component(
+            Multiplication(name="Multi"), graph_id=component_id)
+        assert component_id not in sfg._components_by_id.keys()
+        assert "Multi" in sfg._components_by_name.keys()
+
+    def test_replace_addition_large_tree(self, large_operation_tree):
+        sfg = SFG(outputs=[Output(large_operation_tree)])
+        component_id = "add3"
+
+        sfg = sfg.replace_component(
+            Multiplication(name="Multi"), graph_id=component_id)
+        assert "Multi" in sfg._components_by_name.keys()
+        assert component_id not in sfg._components_by_id.keys()
+
+    def test_replace_no_input_component(self, operation_tree):
+        sfg = SFG(outputs=[Output(operation_tree)])
+        component_id = "c1"
+        const_ = sfg.find_by_id(component_id)
+
+        sfg = sfg.replace_component(Constant(1), graph_id=component_id)
+        assert const_ is not sfg.find_by_id(component_id)
+
+    def test_no_match_on_replace(self, large_operation_tree):
+        sfg = SFG(outputs=[Output(large_operation_tree)])
+        component_id = "addd1"
+
+        try:
+            sfg = sfg.replace_component(
+                Multiplication(name="Multi"), graph_id=component_id)
+        except AssertionError:
+            assert True
+        else:
+            assert False
+
+    def test_not_equal_input(self, large_operation_tree):
+        sfg = SFG(outputs=[Output(large_operation_tree)])
+        component_id = "c1"
+
+        try:
+            sfg = sfg.replace_component(
+                Multiplication(name="Multi"), graph_id=component_id)
+        except AssertionError:
+            assert True
+        else:
+            assert False
+
+
+class TestConstructSFG:
+
+    def test_1k_additions(self):
+        prev_op = Addition(Constant(1), Constant(1))
+        for _ in range(999):
+            prev_op = Addition(prev_op, Constant(2))
+        sfg = SFG(outputs=[Output(prev_op)])
+        sim = FastSimulation(sfg)
+        sim.step()
+        assert sim.results["0"][0].real == 2000
+
+    def test_1k_subtractions(self):
+        prev_op = Subtraction(Constant(0), Constant(2))
+        for _ in range(999):
+            prev_op = Subtraction(prev_op, Constant(2))
+        sfg = SFG(outputs=[Output(prev_op)])
+        sim = FastSimulation(sfg)
+        sim.step()
+        assert sim.results["0"][0].real == -2000
+
+    def test_1k_butterfly(self):
+        prev_op_add = Addition(Constant(1), Constant(1))
+        prev_op_sub = Subtraction(Constant(-1), Constant(1))
+        for _ in range(499):
+            prev_op_add = Addition(prev_op_add, Constant(2))
+        for _ in range(499):
+            prev_op_sub = Subtraction(prev_op_sub, Constant(2))
+        butterfly = Butterfly(prev_op_add, prev_op_sub)
+        sfg = SFG(outputs=[Output(butterfly.output(0)),
+                           Output(butterfly.output(1))])
+        sim = FastSimulation(sfg)
+        sim.step()
+        assert sim.results["0"][0].real == 0
+        assert sim.results["1"][0].real == 2000
+
+    def test_1k_multiplications(self):
+        prev_op = Multiplication(Constant(3), Constant(0.5))
+        for _ in range(999):
+            prev_op = Multiplication(prev_op, Constant(1.01))
+        sfg = SFG(outputs=[Output(prev_op)])
+        sim = FastSimulation(sfg)
+        sim.step()
+        assert sim.results["0"][0].real == 31127.458868040336
+
+    def test_1k_divisions(self):
+        prev_op = Division(Constant(3), Constant(0.5))
+        for _ in range(999):
+            prev_op = Division(prev_op, Constant(1.01))
+        sfg = SFG(outputs=[Output(prev_op)])
+        sim = FastSimulation(sfg)
+        sim.step()
+        assert sim.results["0"][0].real == 0.00028913378500165966
+
+    def test_1k_mins(self):
+        prev_op = Min(Constant(3.14159), Constant(43.14123843))
+        for _ in range(999):
+            prev_op = Min(prev_op, Constant(43.14123843))
+        sfg = SFG(outputs=[Output(prev_op)])
+        sim = FastSimulation(sfg)
+        sim.step()
+        assert sim.results["0"][0].real == 3.14159
+
+    def test_1k_maxs(self):
+        prev_op = Max(Constant(3.14159), Constant(43.14123843))
+        for _ in range(999):
+            prev_op = Max(prev_op, Constant(3.14159))
+        sfg = SFG(outputs=[Output(prev_op)])
+        sim = FastSimulation(sfg)
+        sim.step()
+        assert sim.results["0"][0].real == 43.14123843
+
+    def test_1k_square_roots(self):
+        prev_op = SquareRoot(Constant(1000000))
+        for _ in range(4):
+            prev_op = SquareRoot(prev_op)
+        sfg = SFG(outputs=[Output(prev_op)])
+        sim = FastSimulation(sfg)
+        sim.step()
+        assert sim.results["0"][0].real == 1.539926526059492
+
+    def test_1k_complex_conjugates(self):
+        prev_op = ComplexConjugate(Constant(10+5j))
+        for _ in range(999):
+            prev_op = ComplexConjugate(prev_op)
+        sfg = SFG(outputs=[Output(prev_op)])
+        sim = FastSimulation(sfg)
+        sim.step()
+        assert sim.results["0"] == [10+5j]
+
+    def test_1k_absolutes(self):
+        prev_op = Absolute(Constant(-3.14159))
+        for _ in range(999):
+            prev_op = Absolute(prev_op)
+        sfg = SFG(outputs=[Output(prev_op)])
+        sim = FastSimulation(sfg)
+        sim.step()
+        assert sim.results["0"][0].real == 3.14159
+
+    def test_1k_constant_multiplications(self):
+        prev_op = ConstantMultiplication(1.02, Constant(3.14159))
+        for _ in range(999):
+            prev_op = ConstantMultiplication(1.02, prev_op)
+        sfg = SFG(outputs=[Output(prev_op)])
+        sim = FastSimulation(sfg)
+        sim.step()
+        assert sim.results["0"][0].real == 1251184247.0026844
+
+
+class TestInsertComponent:
+
+    def test_insert_component_in_sfg(self, large_operation_tree_names):
+        sfg = SFG(outputs=[Output(large_operation_tree_names)])
+        sqrt = SquareRoot()
+
+        _sfg = sfg.insert_operation(
+            sqrt, sfg.find_by_name("constant4")[0].graph_id)
+        assert _sfg.evaluate() != sfg.evaluate()
+
+        assert any([isinstance(comp, SquareRoot) for comp in _sfg.operations])
+        assert not any([isinstance(comp, SquareRoot)
+                        for comp in sfg.operations])
+
+        assert not isinstance(sfg.find_by_name("constant4")[0].output(
+            0).signals[0].destination.operation, SquareRoot)
+        assert isinstance(_sfg.find_by_name("constant4")[0].output(
+            0).signals[0].destination.operation, SquareRoot)
+
+        assert sfg.find_by_name("constant4")[0].output(
+            0).signals[0].destination.operation is sfg.find_by_id("add3")
+        assert _sfg.find_by_name("constant4")[0].output(
+            0).signals[0].destination.operation is not _sfg.find_by_id("add3")
+        assert _sfg.find_by_id("sqrt1").output(
+            0).signals[0].destination.operation is _sfg.find_by_id("add3")
+
+    def test_insert_invalid_component_in_sfg(self, large_operation_tree):
+        sfg = SFG(outputs=[Output(large_operation_tree)])
+
+        # Should raise an exception for not matching input count to output count.
+        add4 = Addition()
+        with pytest.raises(Exception):
+            sfg.insert_operation(add4, "c1")
+
+    def test_insert_at_output(self, large_operation_tree):
+        sfg = SFG(outputs=[Output(large_operation_tree)])
+
+        # Should raise an exception for trying to insert an operation after an output.
+        sqrt = SquareRoot()
+        with pytest.raises(Exception):
+            _sfg = sfg.insert_operation(sqrt, "out1")
+
+    def test_insert_multiple_output_ports(self, butterfly_operation_tree):
+        sfg = SFG(outputs=list(map(Output, butterfly_operation_tree.outputs)))
+        _sfg = sfg.insert_operation(Butterfly(name="n_bfly"), "bfly3")
+
+        assert sfg.evaluate() != _sfg.evaluate()
+
+        assert len(sfg.find_by_name("n_bfly")) == 0
+        assert len(_sfg.find_by_name("n_bfly")) == 1
+
+        # Correctly connected old output -> new input
+        assert _sfg.find_by_name("bfly3")[0].output(
+            0).signals[0].destination.operation is _sfg.find_by_name("n_bfly")[0]
+        assert _sfg.find_by_name("bfly3")[0].output(
+            1).signals[0].destination.operation is _sfg.find_by_name("n_bfly")[0]
+
+        # Correctly connected new input -> old output
+        assert _sfg.find_by_name("n_bfly")[0].input(
+            0).signals[0].source.operation is _sfg.find_by_name("bfly3")[0]
+        assert _sfg.find_by_name("n_bfly")[0].input(
+            1).signals[0].source.operation is _sfg.find_by_name("bfly3")[0]
+
+        # Correctly connected new output -> next input
+        assert _sfg.find_by_name("n_bfly")[0].output(
+            0).signals[0].destination.operation is _sfg.find_by_name("bfly2")[0]
+        assert _sfg.find_by_name("n_bfly")[0].output(
+            1).signals[0].destination.operation is _sfg.find_by_name("bfly2")[0]
+
+        # Correctly connected next input -> new output
+        assert _sfg.find_by_name("bfly2")[0].input(
+            0).signals[0].source.operation is _sfg.find_by_name("n_bfly")[0]
+        assert _sfg.find_by_name("bfly2")[0].input(
+            1).signals[0].source.operation is _sfg.find_by_name("n_bfly")[0]
+
+
+class TestFindComponentsWithTypeName:
+    def test_mac_components(self):
+        inp1 = Input("INP1")
+        inp2 = Input("INP2")
+        inp3 = Input("INP3")
+        add1 = Addition(None, None, "ADD1")
+        add2 = Addition(None, None, "ADD2")
+        mul1 = Multiplication(None, None, "MUL1")
+        out1 = Output(None, "OUT1")
+
+        add1.input(0).connect(inp1, "S1")
+        add1.input(1).connect(inp2, "S2")
+        add2.input(0).connect(add1, "S4")
+        add2.input(1).connect(inp3, "S3")
+        mul1.input(0).connect(add1, "S5")
+        mul1.input(1).connect(add2, "S6")
+        out1.input(0).connect(mul1, "S7")
+
+        mac_sfg = SFG(inputs=[inp1, inp2], outputs=[out1], name="mac_sfg")
+
+        assert {comp.name for comp in mac_sfg.find_by_type_name(
+            inp1.type_name())} == {"INP1", "INP2", "INP3"}
+
+        assert {comp.name for comp in mac_sfg.find_by_type_name(
+            add1.type_name())} == {"ADD1", "ADD2"}
+
+        assert {comp.name for comp in mac_sfg.find_by_type_name(
+            mul1.type_name())} == {"MUL1"}
+
+        assert {comp.name for comp in mac_sfg.find_by_type_name(
+            out1.type_name())} == {"OUT1"}
+
+        assert {comp.name for comp in mac_sfg.find_by_type_name(
+            Signal.type_name())} == {"S1", "S2", "S3", "S4", "S5", "S6", "S7"}
+
+
+class TestGetPrecedenceList:
+
+    def test_inputs_delays(self, precedence_sfg_delays):
+
+        precedence_list = precedence_sfg_delays.get_precedence_list()
+
+        assert len(precedence_list) == 7
+
+        assert set([port.operation.key(port.index, port.operation.name)
+                    for port in precedence_list[0]]) == {"IN1", "T1", "T2"}
+
+        assert set([port.operation.key(port.index, port.operation.name)
+                    for port in precedence_list[1]]) == {"C0", "B1", "B2", "A1", "A2"}
+
+        assert set([port.operation.key(port.index, port.operation.name)
+                    for port in precedence_list[2]]) == {"ADD2", "ADD3"}
+
+        assert set([port.operation.key(port.index, port.operation.name)
+                    for port in precedence_list[3]]) == {"ADD1"}
+
+        assert set([port.operation.key(port.index, port.operation.name)
+                    for port in precedence_list[4]]) == {"Q1"}
+
+        assert set([port.operation.key(port.index, port.operation.name)
+                    for port in precedence_list[5]]) == {"A0"}
+
+        assert set([port.operation.key(port.index, port.operation.name)
+                    for port in precedence_list[6]]) == {"ADD4"}
+
+    def test_inputs_constants_delays_multiple_outputs(self, precedence_sfg_delays_and_constants):
+
+        precedence_list = precedence_sfg_delays_and_constants.get_precedence_list()
+
+        assert len(precedence_list) == 7
+
+        assert set([port.operation.key(port.index, port.operation.name)
+                    for port in precedence_list[0]]) == {"IN1", "T1", "CONST1"}
+
+        assert set([port.operation.key(port.index, port.operation.name)
+                    for port in precedence_list[1]]) == {"C0", "B1", "B2", "A1", "A2"}
+
+        assert set([port.operation.key(port.index, port.operation.name)
+                    for port in precedence_list[2]]) == {"ADD2", "ADD3"}
+
+        assert set([port.operation.key(port.index, port.operation.name)
+                    for port in precedence_list[3]]) == {"ADD1"}
+
+        assert set([port.operation.key(port.index, port.operation.name)
+                    for port in precedence_list[4]]) == {"Q1"}
+
+        assert set([port.operation.key(port.index, port.operation.name)
+                    for port in precedence_list[5]]) == {"A0"}
+
+        assert set([port.operation.key(port.index, port.operation.name)
+                    for port in precedence_list[6]]) == {"BFLY1.0", "BFLY1.1"}
+
+    def test_precedence_multiple_outputs_same_precedence(self, sfg_two_inputs_two_outputs):
+        sfg_two_inputs_two_outputs.name = "NESTED_SFG"
+
+        in1 = Input("IN1")
+        sfg_two_inputs_two_outputs.input(0).connect(in1, "S1")
+        in2 = Input("IN2")
+        cmul1 = ConstantMultiplication(10, None, "CMUL1")
+        cmul1.input(0).connect(in2, "S2")
+        sfg_two_inputs_two_outputs.input(1).connect(cmul1, "S3")
+
+        out1 = Output(sfg_two_inputs_two_outputs.output(0), "OUT1")
+        out2 = Output(sfg_two_inputs_two_outputs.output(1), "OUT2")
+
+        sfg = SFG(inputs=[in1, in2], outputs=[out1, out2])
+
+        precedence_list = sfg.get_precedence_list()
+
+        assert len(precedence_list) == 3
+
+        assert set([port.operation.key(port.index, port.operation.name)
+                    for port in precedence_list[0]]) == {"IN1", "IN2"}
+
+        assert set([port.operation.key(port.index, port.operation.name)
+                    for port in precedence_list[1]]) == {"CMUL1"}
+
+        assert set([port.operation.key(port.index, port.operation.name)
+                    for port in precedence_list[2]]) == {"NESTED_SFG.0", "NESTED_SFG.1"}
+
+    def test_precedence_sfg_multiple_outputs_different_precedences(self, sfg_two_inputs_two_outputs_independent):
+        sfg_two_inputs_two_outputs_independent.name = "NESTED_SFG"
+
+        in1 = Input("IN1")
+        in2 = Input("IN2")
+        sfg_two_inputs_two_outputs_independent.input(0).connect(in1, "S1")
+        cmul1 = ConstantMultiplication(10, None, "CMUL1")
+        cmul1.input(0).connect(in2, "S2")
+        sfg_two_inputs_two_outputs_independent.input(1).connect(cmul1, "S3")
+        out1 = Output(sfg_two_inputs_two_outputs_independent.output(0), "OUT1")
+        out2 = Output(sfg_two_inputs_two_outputs_independent.output(1), "OUT2")
+
+        sfg = SFG(inputs=[in1, in2], outputs=[out1, out2])
+
+        precedence_list = sfg.get_precedence_list()
+
+        assert len(precedence_list) == 3
+
+        assert set([port.operation.key(port.index, port.operation.name)
+                    for port in precedence_list[0]]) == {"IN1", "IN2"}
+
+        assert set([port.operation.key(port.index, port.operation.name)
+                    for port in precedence_list[1]]) == {"CMUL1"}
+
+        assert set([port.operation.key(port.index, port.operation.name)
+                    for port in precedence_list[2]]) == {"NESTED_SFG.0", "NESTED_SFG.1"}
+
+
+class TestPrintPrecedence:
+    def test_delays(self, precedence_sfg_delays):
+        sfg = precedence_sfg_delays
+
+        captured_output = io.StringIO()
+        sys.stdout = captured_output
+
+        sfg.print_precedence_graph()
+
+        sys.stdout = sys.__stdout__
+
+        captured_output = captured_output.getvalue()
+
+        assert captured_output == \
+            "-" * 120 + "\n" + \
+            "1.1 \t" + str(sfg.find_by_name("IN1")[0]) + "\n" + \
+            "1.2 \t" + str(sfg.find_by_name("T1")[0]) + "\n" + \
+            "1.3 \t" + str(sfg.find_by_name("T2")[0]) + "\n" + \
+            "-" * 120 + "\n" + \
+            "2.1 \t" + str(sfg.find_by_name("C0")[0]) + "\n" + \
+            "2.2 \t" + str(sfg.find_by_name("A1")[0]) + "\n" + \
+            "2.3 \t" + str(sfg.find_by_name("B1")[0]) + "\n" + \
+            "2.4 \t" + str(sfg.find_by_name("A2")[0]) + "\n" + \
+            "2.5 \t" + str(sfg.find_by_name("B2")[0]) + "\n" + \
+            "-" * 120 + "\n" + \
+            "3.1 \t" + str(sfg.find_by_name("ADD3")[0]) + "\n" + \
+            "3.2 \t" + str(sfg.find_by_name("ADD2")[0]) + "\n" + \
+            "-" * 120 + "\n" + \
+            "4.1 \t" + str(sfg.find_by_name("ADD1")[0]) + "\n" + \
+            "-" * 120 + "\n" + \
+            "5.1 \t" + str(sfg.find_by_name("Q1")[0]) + "\n" + \
+            "-" * 120 + "\n" + \
+            "6.1 \t" + str(sfg.find_by_name("A0")[0]) + "\n" + \
+            "-" * 120 + "\n" + \
+            "7.1 \t" + str(sfg.find_by_name("ADD4")[0]) + "\n" + \
+            "-" * 120 + "\n"
+
+
+class TestDepends:
+    def test_depends_sfg(self, sfg_two_inputs_two_outputs):
+        assert set(sfg_two_inputs_two_outputs.inputs_required_for_output(0)) == {
+            0, 1}
+        assert set(sfg_two_inputs_two_outputs.inputs_required_for_output(1)) == {
+            0, 1}
+
+    def test_depends_sfg_independent(self, sfg_two_inputs_two_outputs_independent):
+        assert set(
+            sfg_two_inputs_two_outputs_independent.inputs_required_for_output(0)) == {0}
+        assert set(
+            sfg_two_inputs_two_outputs_independent.inputs_required_for_output(1)) == {1}
+
+
+class TestConnectExternalSignalsToComponentsSoloComp:
+
+    def test_connect_external_signals_to_components_mac(self):
+        """ Replace a MAC with inner components in an SFG """
+        inp1 = Input("INP1")
+        inp2 = Input("INP2")
+        inp3 = Input("INP3")
+        add1 = Addition(None, None, "ADD1")
+        add2 = Addition(None, None, "ADD2")
+        mul1 = Multiplication(None, None, "MUL1")
+        out1 = Output(None, "OUT1")
+
+        add1.input(0).connect(inp1, "S1")
+        add1.input(1).connect(inp2, "S2")
+        add2.input(0).connect(add1, "S3")
+        add2.input(1).connect(inp3, "S4")
+        mul1.input(0).connect(add1, "S5")
+        mul1.input(1).connect(add2, "S6")
+        out1.input(0).connect(mul1, "S7")
+
+        mac_sfg = SFG(inputs=[inp1, inp2], outputs=[out1])
+
+        inp4 = Input("INP4")
+        inp5 = Input("INP5")
+        out2 = Output(None, "OUT2")
+
+        mac_sfg.input(0).connect(inp4, "S8")
+        mac_sfg.input(1).connect(inp5, "S9")
+        out2.input(0).connect(mac_sfg.outputs[0], "S10")
+
+        test_sfg = SFG(inputs=[inp4, inp5], outputs=[out2])
+        assert test_sfg.evaluate(1, 2) == 9
+        mac_sfg.connect_external_signals_to_components()
+        assert test_sfg.evaluate(1, 2) == 9
+        assert not test_sfg.connect_external_signals_to_components()
+
+    def test_connect_external_signals_to_components_operation_tree(self, operation_tree):
+        """ Replaces an SFG with only a operation_tree component with its inner components """
+        sfg1 = SFG(outputs=[Output(operation_tree)])
+        out1 = Output(None, "OUT1")
+        out1.input(0).connect(sfg1.outputs[0], "S1")
+        test_sfg = SFG(outputs=[out1])
+        assert test_sfg.evaluate_output(0, []) == 5
+        sfg1.connect_external_signals_to_components()
+        assert test_sfg.evaluate_output(0, []) == 5
+        assert not test_sfg.connect_external_signals_to_components()
+
+    def test_connect_external_signals_to_components_large_operation_tree(self, large_operation_tree):
+        """ Replaces an SFG with only a large_operation_tree component with its inner components """
+        sfg1 = SFG(outputs=[Output(large_operation_tree)])
+        out1 = Output(None, "OUT1")
+        out1.input(0).connect(sfg1.outputs[0], "S1")
+        test_sfg = SFG(outputs=[out1])
+        assert test_sfg.evaluate_output(0, []) == 14
+        sfg1.connect_external_signals_to_components()
+        assert test_sfg.evaluate_output(0, []) == 14
+        assert not test_sfg.connect_external_signals_to_components()
+
+
+class TestConnectExternalSignalsToComponentsMultipleComp:
+
+    def test_connect_external_signals_to_components_operation_tree(self, operation_tree):
+        """ Replaces a operation_tree in an SFG with other components """
+        sfg1 = SFG(outputs=[Output(operation_tree)])
+
+        inp1 = Input("INP1")
+        inp2 = Input("INP2")
+        out1 = Output(None, "OUT1")
+
+        add1 = Addition(None, None, "ADD1")
+        add2 = Addition(None, None, "ADD2")
+
+        add1.input(0).connect(inp1, "S1")
+        add1.input(1).connect(inp2, "S2")
+        add2.input(0).connect(add1, "S3")
+        add2.input(1).connect(sfg1.outputs[0], "S4")
+        out1.input(0).connect(add2, "S5")
+
+        test_sfg = SFG(inputs=[inp1, inp2], outputs=[out1])
+        assert test_sfg.evaluate(1, 2) == 8
+        sfg1.connect_external_signals_to_components()
+        assert test_sfg.evaluate(1, 2) == 8
+        assert not test_sfg.connect_external_signals_to_components()
+
+    def test_connect_external_signals_to_components_large_operation_tree(self, large_operation_tree):
+        """ Replaces a large_operation_tree in an SFG with other components """
+        sfg1 = SFG(outputs=[Output(large_operation_tree)])
+
+        inp1 = Input("INP1")
+        inp2 = Input("INP2")
+        out1 = Output(None, "OUT1")
+        add1 = Addition(None, None, "ADD1")
+        add2 = Addition(None, None, "ADD2")
+
+        add1.input(0).connect(inp1, "S1")
+        add1.input(1).connect(inp2, "S2")
+        add2.input(0).connect(add1, "S3")
+        add2.input(1).connect(sfg1.outputs[0], "S4")
+        out1.input(0).connect(add2, "S5")
+
+        test_sfg = SFG(inputs=[inp1, inp2], outputs=[out1])
+        assert test_sfg.evaluate(1, 2) == 17
+        sfg1.connect_external_signals_to_components()
+        assert test_sfg.evaluate(1, 2) == 17
+        assert not test_sfg.connect_external_signals_to_components()
+
+    def create_sfg(self, op_tree):
+        """ Create a simple SFG with either operation_tree or large_operation_tree """
+        sfg1 = SFG(outputs=[Output(op_tree)])
+
+        inp1 = Input("INP1")
+        inp2 = Input("INP2")
+        out1 = Output(None, "OUT1")
+        add1 = Addition(None, None, "ADD1")
+        add2 = Addition(None, None, "ADD2")
+
+        add1.input(0).connect(inp1, "S1")
+        add1.input(1).connect(inp2, "S2")
+        add2.input(0).connect(add1, "S3")
+        add2.input(1).connect(sfg1.outputs[0], "S4")
+        out1.input(0).connect(add2, "S5")
+
+        return SFG(inputs=[inp1, inp2], outputs=[out1])
+
+    def test_connect_external_signals_to_components_many_op(self, large_operation_tree):
+        """ Replaces an sfg component in a larger SFG with several component operations """
+        inp1 = Input("INP1")
+        inp2 = Input("INP2")
+        inp3 = Input("INP3")
+        inp4 = Input("INP4")
+        out1 = Output(None, "OUT1")
+        add1 = Addition(None, None, "ADD1")
+        sub1 = Subtraction(None, None, "SUB1")
+
+        add1.input(0).connect(inp1, "S1")
+        add1.input(1).connect(inp2, "S2")
+
+        sfg1 = self.create_sfg(large_operation_tree)
+
+        sfg1.input(0).connect(add1, "S3")
+        sfg1.input(1).connect(inp3, "S4")
+        sub1.input(0).connect(sfg1.outputs[0], "S5")
+        sub1.input(1).connect(inp4, "S6")
+        out1.input(0).connect(sub1, "S7")
+
+        test_sfg = SFG(inputs=[inp1, inp2, inp3, inp4], outputs=[out1])
+
+        assert test_sfg.evaluate(1, 2, 3, 4) == 16
+        sfg1.connect_external_signals_to_components()
+        assert test_sfg.evaluate(1, 2, 3, 4) == 16
+        assert not test_sfg.connect_external_signals_to_components()
+
+
+class TestTopologicalOrderOperations:
+    def test_feedback_sfg(self, sfg_simple_filter):
+        topological_order = sfg_simple_filter.get_operations_topological_order()
+
+        assert [comp.name for comp in topological_order] == [
+            "IN1", "ADD1", "T1", "CMUL1", "OUT1"]
+
+    def test_multiple_independent_inputs(self, sfg_two_inputs_two_outputs_independent):
+        topological_order = sfg_two_inputs_two_outputs_independent.get_operations_topological_order()
+
+        assert [comp.name for comp in topological_order] == [
+            "IN1", "OUT1", "IN2", "C1", "ADD1", "OUT2"]
+
+    def test_complex_graph(self, precedence_sfg_delays):
+        topological_order = precedence_sfg_delays.get_operations_topological_order()
+
+        assert [comp.name for comp in topological_order] == \
+            ['IN1', 'C0', 'ADD1', 'Q1', 'A0', 'T1', 'B1', 'A1',
+                'T2', 'B2', 'ADD2', 'A2', 'ADD3', 'ADD4', 'OUT1']
+
+
+class TestRemove:
+    def test_remove_single_input_outputs(self, sfg_simple_filter):
+        new_sfg = sfg_simple_filter.remove_operation("cmul1")
+
+        assert set(op.name for op in sfg_simple_filter.find_by_name(
+            "T1")[0].subsequent_operations) == {"CMUL1", "OUT1"}
+        assert set(op.name for op in new_sfg.find_by_name("T1")[
+                   0].subsequent_operations) == {"ADD1", "OUT1"}
+
+        assert set(op.name for op in sfg_simple_filter.find_by_name(
+            "ADD1")[0].preceding_operations) == {"CMUL1", "IN1"}
+        assert set(op.name for op in new_sfg.find_by_name(
+            "ADD1")[0].preceding_operations) == {"T1", "IN1"}
+
+        assert "S1" in set(
+            [sig.name for sig in sfg_simple_filter.find_by_name("T1")[0].output(0).signals])
+        assert "S2" in set(
+            [sig.name for sig in new_sfg.find_by_name("T1")[0].output(0).signals])
+
+    def test_remove_multiple_inputs_outputs(self, butterfly_operation_tree):
+        out1 = Output(butterfly_operation_tree.output(0), "OUT1")
+        out2 = Output(butterfly_operation_tree.output(1), "OUT2")
+
+        sfg = SFG(outputs=[out1, out2])
+
+        new_sfg = sfg.remove_operation(sfg.find_by_name("bfly2")[0].graph_id)
+
+        assert sfg.find_by_name("bfly3")[0].output(0).signal_count == 1
+        assert new_sfg.find_by_name("bfly3")[0].output(0).signal_count == 1
+
+        sfg_dest_0 = sfg.find_by_name(
+            "bfly3")[0].output(0).signals[0].destination
+        new_sfg_dest_0 = new_sfg.find_by_name(
+            "bfly3")[0].output(0).signals[0].destination
+
+        assert sfg_dest_0.index == 0
+        assert new_sfg_dest_0.index == 0
+        assert sfg_dest_0.operation.name == "bfly2"
+        assert new_sfg_dest_0.operation.name == "bfly1"
+
+        assert sfg.find_by_name("bfly3")[0].output(1).signal_count == 1
+        assert new_sfg.find_by_name("bfly3")[0].output(1).signal_count == 1
+
+        sfg_dest_1 = sfg.find_by_name(
+            "bfly3")[0].output(1).signals[0].destination
+        new_sfg_dest_1 = new_sfg.find_by_name(
+            "bfly3")[0].output(1).signals[0].destination
+
+        assert sfg_dest_1.index == 1
+        assert new_sfg_dest_1.index == 1
+        assert sfg_dest_1.operation.name == "bfly2"
+        assert new_sfg_dest_1.operation.name == "bfly1"
+
+        assert sfg.find_by_name("bfly1")[0].input(0).signal_count == 1
+        assert new_sfg.find_by_name("bfly1")[0].input(0).signal_count == 1
+
+        sfg_source_0 = sfg.find_by_name("bfly1")[0].input(0).signals[0].source
+        new_sfg_source_0 = new_sfg.find_by_name(
+            "bfly1")[0].input(0).signals[0].source
+
+        assert sfg_source_0.index == 0
+        assert new_sfg_source_0.index == 0
+        assert sfg_source_0.operation.name == "bfly2"
+        assert new_sfg_source_0.operation.name == "bfly3"
+
+        sfg_source_1 = sfg.find_by_name("bfly1")[0].input(1).signals[0].source
+        new_sfg_source_1 = new_sfg.find_by_name(
+            "bfly1")[0].input(1).signals[0].source
+
+        assert sfg_source_1.index == 1
+        assert new_sfg_source_1.index == 1
+        assert sfg_source_1.operation.name == "bfly2"
+        assert new_sfg_source_1.operation.name == "bfly3"
+
+        assert "bfly2" not in set(op.name for op in new_sfg.operations)
+
+    def remove_different_number_inputs_outputs(self, sfg_simple_filter):
+        with pytest.raises(ValueError):
+            sfg_simple_filter.remove_operation("add1")
+
+
+class TestSaveLoadSFG:
+    def get_path(self, existing=False):
+        path_ = "".join(random.choices(string.ascii_uppercase, k=4)) + ".py"
+        while path.exists(path_) if not existing else not path.exists(path_):
+            path_ = "".join(random.choices(
+                string.ascii_uppercase, k=4)) + ".py"
+
+        return path_
+
+    def test_save_simple_sfg(self, sfg_simple_filter):
+        result = sfg_to_python(sfg_simple_filter)
+        path_ = self.get_path()
+
+        assert not path.exists(path_)
+        with open(path_, "w") as file_obj:
+            file_obj.write(result)
+
+        assert path.exists(path_)
+
+        with open(path_, "r") as file_obj:
+            assert file_obj.read() == result
+
+        remove(path_)
+
+    def test_save_complex_sfg(self, precedence_sfg_delays_and_constants):
+        result = sfg_to_python(precedence_sfg_delays_and_constants)
+        path_ = self.get_path()
+
+        assert not path.exists(path_)
+        with open(path_, "w") as file_obj:
+            file_obj.write(result)
+
+        assert path.exists(path_)
+
+        with open(path_, "r") as file_obj:
+            assert file_obj.read() == result
+
+        remove(path_)
+
+    def test_load_simple_sfg(self, sfg_simple_filter):
+        result = sfg_to_python(sfg_simple_filter)
+        path_ = self.get_path()
+
+        assert not path.exists(path_)
+        with open(path_, "w") as file_obj:
+            file_obj.write(result)
+
+        assert path.exists(path_)
+
+        simple_filter_, _ = python_to_sfg(path_)
+
+        assert str(sfg_simple_filter) == str(simple_filter_)
+        assert sfg_simple_filter.evaluate([2]) == simple_filter_.evaluate([2])
+
+        remove(path_)
+
+    def test_load_complex_sfg(self, precedence_sfg_delays_and_constants):
+        result = sfg_to_python(precedence_sfg_delays_and_constants)
+        path_ = self.get_path()
+
+        assert not path.exists(path_)
+        with open(path_, "w") as file_obj:
+            file_obj.write(result)
+
+        assert path.exists(path_)
+
+        precedence_sfg_registers_and_constants_, _ = python_to_sfg(path_)
+
+        assert str(precedence_sfg_delays_and_constants) == str(
+            precedence_sfg_registers_and_constants_)
+
+        remove(path_)
+
+    def test_load_invalid_path(self):
+        path_ = self.get_path(existing=False)
+        with pytest.raises(Exception):
+            python_to_sfg(path_)
+
+
+class TestGetComponentsOfType:
+    def test_get_no_operations_of_type(self, sfg_two_inputs_two_outputs):
+        assert [op.name for op in sfg_two_inputs_two_outputs.find_by_type_name(Multiplication.type_name())] \
+            == []
+
+    def test_get_multple_operations_of_type(self, sfg_two_inputs_two_outputs):
+        assert [op.name for op in sfg_two_inputs_two_outputs.find_by_type_name(Addition.type_name())] \
+            == ["ADD1", "ADD2"]
+
+        assert [op.name for op in sfg_two_inputs_two_outputs.find_by_type_name(Input.type_name())] \
+            == ["IN1", "IN2"]
+
+        assert [op.name for op in sfg_two_inputs_two_outputs.find_by_type_name(Output.type_name())] \
+            == ["OUT1", "OUT2"]
diff --git a/test/test_signal.py b/test/test_signal.py
index ab07eb778ddb693bfc9cfabf6aeb7804038312d5..42086d4d5fb68eb861eee72d1351931d473b480b 100644
--- a/test/test_signal.py
+++ b/test/test_signal.py
@@ -2,14 +2,14 @@
 B-ASIC test suit for the signal module which consists of the Signal class.
 """
 
-from b_asic.port import InputPort, OutputPort
-from b_asic.signal import Signal
-
 import pytest
 
+from b_asic import InputPort, OutputPort, Signal
+
+
 def test_signal_creation_and_disconnction_and_connection_changing():
-    in_port = InputPort(0, None)
-    out_port = OutputPort(1, None)
+    in_port = InputPort(None, 0)
+    out_port = OutputPort(None, 1)
     s = Signal(out_port, in_port)
 
     assert in_port.signals == [s]
@@ -17,7 +17,7 @@ def test_signal_creation_and_disconnction_and_connection_changing():
     assert s.source is out_port
     assert s.destination is in_port
 
-    in_port1 = InputPort(0, None)
+    in_port1 = InputPort(None, 0)
     s.set_destination(in_port1)
 
     assert in_port.signals == []
@@ -40,7 +40,7 @@ def test_signal_creation_and_disconnction_and_connection_changing():
     assert s.source is None
     assert s.destination is None
 
-    out_port1 = OutputPort(0, None)
+    out_port1 = OutputPort(None, 0)
     s.set_source(out_port1)
 
     assert out_port1.signals == [s]
@@ -60,3 +60,29 @@ def test_signal_creation_and_disconnction_and_connection_changing():
     assert in_port.signals == [s]
     assert s.source is out_port
     assert s.destination is in_port
+
+class Bits:
+    def test_pos_int(self, signal):
+        signal.bits = 10
+        assert signal.bits == 10
+
+    def test_bits_zero(self, signal):
+        signal.bits = 0
+        assert signal.bits == 0
+
+    def test_bits_neg_int(self, signal):
+        with pytest.raises(Exception):
+            signal.bits = -10
+
+    def test_bits_complex(self, signal):
+        with pytest.raises(Exception):
+            signal.bits = (2+4j)
+
+    def test_bits_float(self, signal):
+        with pytest.raises(Exception):
+            signal.bits = 3.2
+
+    def test_bits_pos_then_none(self, signal):
+        signal.bits = 10
+        signal.bits = None
+        assert signal.bits is None
\ No newline at end of file
diff --git a/test/test_simulation.py b/test/test_simulation.py
new file mode 100644
index 0000000000000000000000000000000000000000..8ca19e47ba862d269402cce9789e12aa795fcf83
--- /dev/null
+++ b/test/test_simulation.py
@@ -0,0 +1,202 @@
+import pytest
+import numpy as np
+
+from b_asic import SFG, Output, Simulation
+
+
+class TestRunFor:
+    def test_with_lambdas_as_input(self, sfg_two_inputs_two_outputs):
+        simulation = Simulation(sfg_two_inputs_two_outputs, [lambda n: n + 3, lambda n: 1 + n * 2])
+
+        output = simulation.run_for(101, save_results = True)
+
+        assert output[0] == 304
+        assert output[1] == 505
+
+        assert simulation.results["0"][100] == 304
+        assert simulation.results["1"][100] == 505
+
+        assert simulation.results["in1"][0] == 3
+        assert simulation.results["in2"][0] == 1
+        assert simulation.results["add1"][0] == 4
+        assert simulation.results["add2"][0] == 5
+        assert simulation.results["0"][0] == 4
+        assert simulation.results["1"][0] == 5
+
+        assert simulation.results["in1"][1] == 4
+        assert simulation.results["in2"][1] == 3
+        assert simulation.results["add1"][1] == 7
+        assert simulation.results["add2"][1] == 10
+        assert simulation.results["0"][1] == 7
+        assert simulation.results["1"][1] == 10
+
+        assert simulation.results["in1"][2] == 5
+        assert simulation.results["in2"][2] == 5
+        assert simulation.results["add1"][2] == 10
+        assert simulation.results["add2"][2] == 15
+        assert simulation.results["0"][2] == 10
+        assert simulation.results["1"][2] == 15
+
+        assert simulation.results["in1"][3] == 6
+        assert simulation.results["in2"][3] == 7
+        assert simulation.results["add1"][3] == 13
+        assert simulation.results["add2"][3] == 20
+        assert simulation.results["0"][3] == 13
+        assert simulation.results["1"][3] == 20
+
+    def test_with_numpy_arrays_as_input(self, sfg_two_inputs_two_outputs):
+        input0 = np.array([5, 9, 25, -5, 7])
+        input1 = np.array([7, 3, 3,  54, 2])
+        simulation = Simulation(sfg_two_inputs_two_outputs, [input0, input1])
+
+        output = simulation.run_for(5, save_results = True)
+
+        assert output[0] == 9
+        assert output[1] == 11
+
+        assert isinstance(simulation.results["in1"], np.ndarray)
+        assert isinstance(simulation.results["in2"], np.ndarray)
+        assert isinstance(simulation.results["add1"], np.ndarray)
+        assert isinstance(simulation.results["add2"], np.ndarray)
+        assert isinstance(simulation.results["0"], np.ndarray)
+        assert isinstance(simulation.results["1"], np.ndarray)
+
+        assert simulation.results["in1"][0] == 5
+        assert simulation.results["in2"][0] == 7
+        assert simulation.results["add1"][0] == 12
+        assert simulation.results["add2"][0] == 19
+        assert simulation.results["0"][0] == 12
+        assert simulation.results["1"][0] == 19
+
+        assert simulation.results["in1"][1] == 9
+        assert simulation.results["in2"][1] == 3
+        assert simulation.results["add1"][1] == 12
+        assert simulation.results["add2"][1] == 15
+        assert simulation.results["0"][1] == 12
+        assert simulation.results["1"][1] == 15
+
+        assert simulation.results["in1"][2] == 25
+        assert simulation.results["in2"][2] == 3
+        assert simulation.results["add1"][2] == 28
+        assert simulation.results["add2"][2] == 31
+        assert simulation.results["0"][2] == 28
+        assert simulation.results["1"][2] == 31
+
+        assert simulation.results["in1"][3] == -5
+        assert simulation.results["in2"][3] == 54
+        assert simulation.results["add1"][3] == 49
+        assert simulation.results["add2"][3] == 103
+        assert simulation.results["0"][3] == 49
+        assert simulation.results["1"][3] == 103
+
+        assert simulation.results["0"][4] == 9
+        assert simulation.results["1"][4] == 11
+    
+    def test_with_numpy_array_overflow(self, sfg_two_inputs_two_outputs):
+        input0 = np.array([5, 9, 25, -5, 7])
+        input1 = np.array([7, 3, 3,  54, 2])
+        simulation = Simulation(sfg_two_inputs_two_outputs, [input0, input1])
+        simulation.run_for(5)
+        with pytest.raises(IndexError):
+            simulation.step()
+
+    def test_run_whole_numpy_array(self, sfg_two_inputs_two_outputs):
+        input0 = np.array([5, 9, 25, -5, 7])
+        input1 = np.array([7, 3, 3,  54, 2])
+        simulation = Simulation(sfg_two_inputs_two_outputs, [input0, input1])
+        simulation.run()
+        assert len(simulation.results["0"]) == 5
+        assert len(simulation.results["1"]) == 5
+        with pytest.raises(IndexError):
+            simulation.step()
+
+    def test_delay(self, sfg_delay):
+        simulation = Simulation(sfg_delay)
+        simulation.set_input(0, [5, -2, 25, -6, 7, 0])
+        simulation.run_for(6, save_results = True)
+
+        assert simulation.results["0"][0] == 0
+        assert simulation.results["0"][1] == 5
+        assert simulation.results["0"][2] == -2
+        assert simulation.results["0"][3] == 25
+        assert simulation.results["0"][4] == -6
+        assert simulation.results["0"][5] == 7
+
+    def test_find_result_key(self, precedence_sfg_delays):
+        sim = Simulation(precedence_sfg_delays, [[0, 4, 542, 42, 31.314, 534.123, -453415, 5431]])
+        sim.run()
+        assert sim.results[precedence_sfg_delays.find_result_keys_by_name("ADD2")[0]][4] == 31220
+        assert sim.results[precedence_sfg_delays.find_result_keys_by_name("A1")[0]][2] == 80
+
+class TestRun:
+    def test_save_results(self, sfg_two_inputs_two_outputs):
+        simulation = Simulation(sfg_two_inputs_two_outputs, [2, 3])
+        assert not simulation.results
+        simulation.run_for(10, save_results = False)
+        assert not simulation.results
+        simulation.run_for(10)
+        assert len(simulation.results["0"]) == 10
+        assert len(simulation.results["1"]) == 10
+        simulation.run_for(10, save_results = True)
+        assert len(simulation.results["0"]) == 20
+        assert len(simulation.results["1"]) == 20
+        simulation.run_for(10, save_results = False)
+        assert len(simulation.results["0"]) == 20
+        assert len(simulation.results["1"]) == 20
+        simulation.run_for(13, save_results = True)
+        assert len(simulation.results["0"]) == 33
+        assert len(simulation.results["1"]) == 33
+        simulation.step(save_results = False)
+        assert len(simulation.results["0"]) == 33
+        assert len(simulation.results["1"]) == 33
+        simulation.step()
+        assert len(simulation.results["0"]) == 34
+        assert len(simulation.results["1"]) == 34
+        simulation.clear_results()
+        assert not simulation.results
+
+    def test_nested(self, sfg_nested):
+        input0 = np.array([5, 9])
+        input1 = np.array([7, 3])
+        simulation = Simulation(sfg_nested, [input0, input1])
+
+        output0 = simulation.step()
+        output1 = simulation.step()
+
+        assert output0[0] == 11405
+        assert output1[0] == 4221
+    
+    def test_accumulator(self, sfg_accumulator):
+        data_in = np.array([5, -2, 25, -6, 7, 0])
+        reset   = np.array([0, 0,  0,  1,  0, 0])
+        simulation = Simulation(sfg_accumulator, [data_in, reset])
+        output0 = simulation.step()
+        output1 = simulation.step()
+        output2 = simulation.step()
+        output3 = simulation.step()
+        output4 = simulation.step()
+        output5 = simulation.step()
+        assert output0[0] == 0
+        assert output1[0] == 5
+        assert output2[0] == 3
+        assert output3[0] == 28
+        assert output4[0] == 0
+        assert output5[0] == 7
+
+    def test_simple_accumulator(self, sfg_simple_accumulator):
+        data_in = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+        simulation = Simulation(sfg_simple_accumulator, [data_in])
+        simulation.run()
+        assert list(simulation.results["0"]) == [0, 1, 3, 6, 10, 15, 21, 28, 36, 45]
+        
+    def test_simple_filter(self, sfg_simple_filter):
+        input0 = np.array([1, 2, 3, 4, 5])
+        simulation = Simulation(sfg_simple_filter, [input0])
+        simulation.run_for(len(input0), save_results = True)
+        assert all(simulation.results["0"] == np.array([0, 1.0, 2.5, 4.25, 6.125]))
+
+    def test_custom_operation(self, sfg_custom_operation):
+        simulation = Simulation(sfg_custom_operation, [lambda n: n + 1])
+        simulation.run_for(5)
+        assert all(simulation.results["0"] == np.array([2, 4, 6, 8, 10]))
+        assert all(simulation.results["1"] == np.array([2, 4, 8, 16, 32]))