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]))