From 1b3465dbf44782110717921834ea4149fee74f83 Mon Sep 17 00:00:00 2001 From: Angus Lothian <anglo547@student.liu.se> Date: Tue, 26 May 2020 17:04:37 +0200 Subject: [PATCH] Change so that Basic Operation also inherits from abstract graph component and removed name property implementation --- .clang-format | 8 +- .gitignore | 69 +- CMakeLists.txt | 56 +- Doxyfile | 2553 +++++++++++++++++ MANIFEST.in | 1 + README.md | 29 +- b_asic/GUI/__init__.py | 15 + b_asic/GUI/about_window.py | 130 + b_asic/GUI/arrow.py | 49 + b_asic/GUI/drag_button.py | 156 + b_asic/GUI/gui_interface.py | 326 +++ b_asic/GUI/gui_interface.ui | 763 +++++ b_asic/GUI/main_window.py | 625 ++++ b_asic/GUI/operation_icons/abs.png | Bin 0 -> 1251 bytes b_asic/GUI/operation_icons/abs_grey.png | Bin 0 -> 1369 bytes b_asic/GUI/operation_icons/add.png | Bin 0 -> 1208 bytes b_asic/GUI/operation_icons/add_grey.png | Bin 0 -> 1300 bytes b_asic/GUI/operation_icons/bfly.png | Bin 0 -> 5358 bytes b_asic/GUI/operation_icons/bfly_grey.png | Bin 0 -> 5138 bytes b_asic/GUI/operation_icons/c.png | Bin 0 -> 5203 bytes b_asic/GUI/operation_icons/c_grey.png | Bin 0 -> 5020 bytes b_asic/GUI/operation_icons/cmul.png | Bin 0 -> 3601 bytes b_asic/GUI/operation_icons/cmul_grey.png | Bin 0 -> 3848 bytes b_asic/GUI/operation_icons/conj.png | Bin 0 -> 3993 bytes b_asic/GUI/operation_icons/conj_grey.png | Bin 0 -> 3887 bytes .../GUI/operation_icons/custom_operation.png | Bin 0 -> 6298 bytes .../operation_icons/custom_operation_grey.png | Bin 0 -> 6457 bytes b_asic/GUI/operation_icons/div.png | Bin 0 -> 1318 bytes b_asic/GUI/operation_icons/div_grey.png | Bin 0 -> 1352 bytes b_asic/GUI/operation_icons/in.png | Bin 0 -> 1951 bytes b_asic/GUI/operation_icons/in_grey.png | Bin 0 -> 2093 bytes b_asic/GUI/operation_icons/max.png | Bin 0 -> 5822 bytes b_asic/GUI/operation_icons/max_grey.png | Bin 0 -> 5639 bytes b_asic/GUI/operation_icons/min.png | Bin 0 -> 3675 bytes b_asic/GUI/operation_icons/min_grey.png | Bin 0 -> 3595 bytes b_asic/GUI/operation_icons/mul.png | Bin 0 -> 2222 bytes b_asic/GUI/operation_icons/mul_grey.png | Bin 0 -> 2383 bytes b_asic/GUI/operation_icons/out.png | Bin 0 -> 1962 bytes b_asic/GUI/operation_icons/out_grey.png | Bin 0 -> 2089 bytes b_asic/GUI/operation_icons/sqrt.png | Bin 0 -> 2994 bytes b_asic/GUI/operation_icons/sqrt_grey.png | Bin 0 -> 2942 bytes b_asic/GUI/operation_icons/sub.png | Bin 0 -> 1155 bytes b_asic/GUI/operation_icons/sub_grey.png | Bin 0 -> 1208 bytes b_asic/GUI/operation_icons/t.png | Bin 0 -> 4369 bytes b_asic/GUI/operation_icons/t_grey.png | Bin 0 -> 4304 bytes b_asic/GUI/port_button.py | 59 + b_asic/GUI/properties_window.py | 146 + b_asic/GUI/select_sfg_window.py | 39 + b_asic/GUI/show_pc_window.py | 49 + b_asic/GUI/simulate_sfg_window.py | 175 ++ b_asic/GUI/utils.py | 20 + b_asic/__init__.py | 17 +- b_asic/core_operations.py | 402 ++- b_asic/graph_component.py | 139 +- b_asic/graph_id.py | 26 - b_asic/operation.py | 702 +++-- b_asic/port.py | 233 +- b_asic/precedence_chart.py | 21 - b_asic/save_load_structure.py | 90 + b_asic/schema.py | 117 +- b_asic/signal.py | 100 +- b_asic/signal_flow_graph.py | 858 +++++- b_asic/simulation.py | 134 +- b_asic/special_operations.py | 117 + legacy/README.md | 11 + legacy/simulation_oop/core_operations.h | 236 ++ legacy/simulation_oop/custom_operation.cpp | 30 + legacy/simulation_oop/custom_operation.h | 35 + legacy/simulation_oop/operation.cpp | 156 + legacy/simulation_oop/operation.h | 132 + legacy/simulation_oop/signal_flow_graph.cpp | 144 + legacy/simulation_oop/signal_flow_graph.h | 82 + legacy/simulation_oop/simulation.cpp | 138 + legacy/simulation_oop/simulation.h | 66 + legacy/simulation_oop/special_operations.cpp | 78 + legacy/simulation_oop/special_operations.h | 55 + logo_tiny.png | Bin 0 -> 10071 bytes setup.py | 60 +- small_logo.png | Bin 0 -> 40475 bytes src/algorithm.h | 325 +++ src/debug.h | 80 + src/main.cpp | 28 +- src/number.h | 13 + src/simulation.cpp | 61 + src/simulation.h | 12 + src/simulation/compile.cpp | 313 ++ src/simulation/compile.h | 61 + src/simulation/format_code.h | 129 + src/simulation/instruction.h | 57 + src/simulation/run.cpp | 176 ++ src/simulation/run.h | 23 + src/simulation/simulation.cpp | 129 + src/simulation/simulation.h | 54 + src/span.h | 314 ++ test/conftest.py | 3 +- test/fixtures/operation_tree.py | 121 +- test/fixtures/port.py | 16 +- test/fixtures/signal.py | 4 +- test/fixtures/signal_flow_graph.py | 245 ++ test/operation/test_abstract_operation.py | 77 - test/test_core_operations.py | 395 ++- test/test_fast_simulation.py | 232 ++ test/test_graph_id_generator.py | 19 +- test/test_inputport.py | 102 +- test/test_operation.py | 185 +- test/test_outputport.py | 72 +- test/test_schema.py | 67 + test/test_sfg.py | 1018 +++++++ test/test_signal.py | 40 +- test/test_simulation.py | 202 ++ 110 files changed, 12730 insertions(+), 1290 deletions(-) create mode 100644 Doxyfile create mode 100644 b_asic/GUI/__init__.py create mode 100644 b_asic/GUI/about_window.py create mode 100644 b_asic/GUI/arrow.py create mode 100644 b_asic/GUI/drag_button.py create mode 100644 b_asic/GUI/gui_interface.py create mode 100644 b_asic/GUI/gui_interface.ui create mode 100644 b_asic/GUI/main_window.py create mode 100644 b_asic/GUI/operation_icons/abs.png create mode 100644 b_asic/GUI/operation_icons/abs_grey.png create mode 100644 b_asic/GUI/operation_icons/add.png create mode 100644 b_asic/GUI/operation_icons/add_grey.png create mode 100644 b_asic/GUI/operation_icons/bfly.png create mode 100644 b_asic/GUI/operation_icons/bfly_grey.png create mode 100644 b_asic/GUI/operation_icons/c.png create mode 100644 b_asic/GUI/operation_icons/c_grey.png create mode 100644 b_asic/GUI/operation_icons/cmul.png create mode 100644 b_asic/GUI/operation_icons/cmul_grey.png create mode 100644 b_asic/GUI/operation_icons/conj.png create mode 100644 b_asic/GUI/operation_icons/conj_grey.png create mode 100644 b_asic/GUI/operation_icons/custom_operation.png create mode 100644 b_asic/GUI/operation_icons/custom_operation_grey.png create mode 100644 b_asic/GUI/operation_icons/div.png create mode 100644 b_asic/GUI/operation_icons/div_grey.png create mode 100644 b_asic/GUI/operation_icons/in.png create mode 100644 b_asic/GUI/operation_icons/in_grey.png create mode 100644 b_asic/GUI/operation_icons/max.png create mode 100644 b_asic/GUI/operation_icons/max_grey.png create mode 100644 b_asic/GUI/operation_icons/min.png create mode 100644 b_asic/GUI/operation_icons/min_grey.png create mode 100644 b_asic/GUI/operation_icons/mul.png create mode 100644 b_asic/GUI/operation_icons/mul_grey.png create mode 100644 b_asic/GUI/operation_icons/out.png create mode 100644 b_asic/GUI/operation_icons/out_grey.png create mode 100644 b_asic/GUI/operation_icons/sqrt.png create mode 100644 b_asic/GUI/operation_icons/sqrt_grey.png create mode 100644 b_asic/GUI/operation_icons/sub.png create mode 100644 b_asic/GUI/operation_icons/sub_grey.png create mode 100644 b_asic/GUI/operation_icons/t.png create mode 100644 b_asic/GUI/operation_icons/t_grey.png create mode 100644 b_asic/GUI/port_button.py create mode 100644 b_asic/GUI/properties_window.py create mode 100644 b_asic/GUI/select_sfg_window.py create mode 100644 b_asic/GUI/show_pc_window.py create mode 100644 b_asic/GUI/simulate_sfg_window.py create mode 100644 b_asic/GUI/utils.py delete mode 100644 b_asic/graph_id.py delete mode 100644 b_asic/precedence_chart.py create mode 100644 b_asic/save_load_structure.py create mode 100644 b_asic/special_operations.py create mode 100644 legacy/README.md create mode 100644 legacy/simulation_oop/core_operations.h create mode 100644 legacy/simulation_oop/custom_operation.cpp create mode 100644 legacy/simulation_oop/custom_operation.h create mode 100644 legacy/simulation_oop/operation.cpp create mode 100644 legacy/simulation_oop/operation.h create mode 100644 legacy/simulation_oop/signal_flow_graph.cpp create mode 100644 legacy/simulation_oop/signal_flow_graph.h create mode 100644 legacy/simulation_oop/simulation.cpp create mode 100644 legacy/simulation_oop/simulation.h create mode 100644 legacy/simulation_oop/special_operations.cpp create mode 100644 legacy/simulation_oop/special_operations.h create mode 100644 logo_tiny.png create mode 100644 small_logo.png create mode 100644 src/algorithm.h create mode 100644 src/debug.h create mode 100644 src/number.h create mode 100644 src/simulation.cpp create mode 100644 src/simulation.h create mode 100644 src/simulation/compile.cpp create mode 100644 src/simulation/compile.h create mode 100644 src/simulation/format_code.h create mode 100644 src/simulation/instruction.h create mode 100644 src/simulation/run.cpp create mode 100644 src/simulation/run.h create mode 100644 src/simulation/simulation.cpp create mode 100644 src/simulation/simulation.h create mode 100644 src/span.h create mode 100644 test/fixtures/signal_flow_graph.py delete mode 100644 test/operation/test_abstract_operation.py create mode 100644 test/test_fast_simulation.py create mode 100644 test/test_schema.py create mode 100644 test/test_sfg.py create mode 100644 test/test_simulation.py diff --git a/.clang-format b/.clang-format index 7548f76b..22e04bab 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 0bdf5476..c1e09d2e 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 3471fb4f..3f8304e3 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 00000000..6ee7c4a5 --- /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 ce996f6c..96b265fd 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 fd98f919..db11db76 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 00000000..f330b589 --- /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 00000000..699e1b7c --- /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 00000000..7df52d05 --- /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 00000000..a1d8118e --- /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 00000000..bfbe76f4 --- /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 00000000..38274781 --- /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 00000000..630e4af0 --- /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 GIT binary patch literal 1251 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST~P>fw3{u**Ty%$lXc7)79C`(9+CI z*GSKhfk9(p>BRHeE`cJ)?Tt_5FZK`<o4jGxMF~~2Q$A8&vyN#rpFLx2f8|fbUh_#g zQ`as!a%??oJ!5q0T9Jv9rnZ`HOsqOL+uU@L<@Vz9&+FdXF0Ve%T5Yj5<x3WS%BD(P zkq?&7l|G-lCBEwAf_Y_8i@MhJu{H;XY&l<@leVXH_ExR=zpqP8f3%Bfo7KZW*Q=in z2_FhxU3vA1?87ou(O4ln;j+vk$A{Lz{gHpNiY`6MpS-o;1pl|N#Pm<%$Llsux@2_d zQ&E-COVOnz@}E_GKfZa>b0qJhUWpUShleSX3jgpJ9_TpZyyN2~eQm8L)m=X?KT!#v z7@5l3n>uO9iN`{vuB(o#s=4%^_Sl^E?SFp!*CPwAue$f7c~<)2iubLzay4=!k6oXb zDAKp^6{p_jr+@A!^3-?i3%ypwcVHvq^YB#NZQSCsFZ^8LwlyPgmC$x|p$}zTX9Y8M zH@2~^PG4H<C#SGSFTRww`u7F(=d#`Q+@a}v7Os3T9~dW`1s;*b3=G`DAk4@xYmNj1 z12c!Gi(^Q|oVT|Py_6Fr8XnHPdd)6$gW!~p8ut{h3+(CY-MZDBbK`lzJqL2OZW5}z zy2dSWK6}ua1Ltdw{x_;Ol$?J0>E*KBwvr99`}b8d{5Z+N^zg^l*^e#cs`!2gKC7%N z+$(&dYOma~Osg9XA13=;HpwxY{XE-Uhl8n6K>#1|;njb~Q#R+V@1NO!-J<Vb_91)A zFaLgjkbd&(SCxg8)ved%b#?zP9zV!!Zf;)ve*fmpoB8d}3vxF(C~&Y09pcc(a{dqd zckbRT{qFty?@_xWblPs;zU@D+LumqYbxn<ooSfXZsOqY!Upo(fP$L*>`<~fu=<e=* z`07>G`mNnZA5C8VP?yPa!__R)HDRmY%Qcr59cjWJ>Vr<!KyUi#<!{TXKRrJNPDZC( zKij<jzM=3=w(Os`+TZV+_#RH%T)EHg-zxTl;g(PS6%*k@>-oFm<KpU$%Y0C-Y<3Xf eU}<#t!TWoT-Kn+rHu?iI3WKMspUXO@geCwV2+aoo literal 0 HcmV?d00001 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 GIT binary patch literal 1369 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST~P>fw4K$**Ty%$lXc7)79C`(9+CI z*GSKhfk9(p>BRHeE`cJ)?LRk9l<!Si7_rA>ic{T{BW{wMs=mon;%Cn^-+$#y#a{DE zDXVitRn6*je{kQ>%3j(rWsT07PTtGK_s(4LF?wG1?&qENd!LuQ-<iO#c6G|bH_ahd zi`ms`PW?Hw=uy>CPwuN*E{O*5=3aR}f7zm1+l0-JV*5i^f7)5UR!_(@WW!aC{uQ(4 z3C<Il7k55G>^+zH>dpX{A2B<kr*m`NkFMHrAYbe__n+jAay&hadjlgnrx)(%x!N*M zXxjA0o~qZcJgQwY{fhhL;>|+0kKQ)mFlgj&)jTG7*xUZFQT5BjhPcQ;#r^v8?GLYd zvdSns<zl3wUlY&WMe97jt~fH|MWCeTrm+A2zg_L%<r7(T{r7@bM@{Ul4s2!KnS1$< zo!QwtoEGyQYQ>(4w{2SJe{s9_tmguEW)~F3UOH4=u#;8SKgxsuscQQUTe0Tv#u*U} z%c57rYJFL=<NQly7V{^o^Ai3uSUufS{Zl-6OZr{2*?(69V}-N8BeIx*fm;}a85w5H zkzin8uJLqn45^s&_V!+{*hq<nhf^*8u{?1I2+D9-<|*niQ7v5I^6iLcO_!>LcQOa_ ztkiXh_LOYn;c)bss9+S@)$^fkv*6j3dCTv7*tqXmYv`K43F+z2&wl;-^*!^Cb2(<e zi(h_ze!hOr{Q2kKS4;l5nUkNt{!iI`?*r$36a=RI`<qx_U%zJk`uoTA3fAA-UvJOH z&%gh7nev3^=jZpYUAxvgnyXH1^UXJI3hN>SSeP6Y@DUG>6@Qnn{QmB)g`M5Jk{i#S zJ-c!L{(KYe<etKU0)u^Zf2}gh%FD}bs=wtdoY~6!^TWf#7M7NgB{@BY$DTfYy2SFw zvoBvtYJPr7z4WGATwiQn;n5}q0S>0dp+b0n-+SQy+1uOmk00-sS3hf4`>SLR&^y!2 z7jZo4la-b2-Mo3T=jQP6aHgHf1jBRBv%Sgw^K2x6$|tAE+t<~czWqTNsBwMV-d*oS zBO0{DSQ;G!IED%l5moZw;o<gVyZQcBIk~yHmt<;;=bt}*>eQ(vH-VAgnl8qdlAoXd z=IvYE8i6})*Vo72e_2T|kXV>NiOzq%oo`(}|Nm5RadGo~wl{9v*kNIBKmQxM$e%;o zwr}^3-KfC9a_G{lSFi5(%h^W#xE}rE>e;l-`~NW}`-XUGuuW42W=;lAS3j3^P6<r_ DpbP}7 literal 0 HcmV?d00001 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 GIT binary patch literal 1208 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU~J5Eb`B^Ga(7bjbagf}v@|o* zHPSO=V9=OYI`O==OQ6Vcd*c)Ni#^1|CU2N^Q9{-1l#i6xtYaF@XU`bhU-?t9*L+gW z)U}I_99z#?&lsJ$R%GI&sja3P6RXb6HaDGQxxM)O^Sbx8%d5|`R$Hu1`I5z-vZ+#6 z<b&mNrO)SXiLZLOU|w0&qONs)tj)n8Th3SKr0pr4y;W=e@9R?2AMIk=X7w=8_3EcX z!iR!aS6+Q0`>;$^G*-w?xGb~C@u78af8?L6qDznRCvPn{!T&8RG5wSH@w$zZE*TyA zR8(d3QgmsF{AX3)k8j@e9Lf8rSK`F-;bF?8!aqEQ2RhC;@A!C0Ut8-*b=S|!PgKGu zMyB%ircPRN;;~Sv>#F0bYA*e!JvOI(`=1~G^~i$jtL{B%o|S&M;(hC_T#X#bW7j7p ziu5gf#i_UX>7P4_JoO#>La$Zv9oWeDJUmr*8@KrE3qM!5ZOsT=CA3{#=tCLTS;36m zjcu%})0fuz$tmp7i!bG^{(V9Hxoo#RcWC;ag)3jo2gV6!fk$L90|U1(2s1Lwnj^u$ zz;xHs#WAE}&f8moUebvo4T0&iSYP-(;!|OX+4AE7(~+$^8h(6V7;ri0mf^3`MRPuz z6);c#VEDO6Uyp&|N5<ukhb5BN+WvTROyaZ6|C%}fjkwf>#W)iu3g9L>&)(vmvv~US z>E>tU=XZZ-u7AQ`w)^h(qj~xH*YACo;p}eN;K4D3h$AI`430(oJAL@!hWqdBtLAV1 z@a4;wKY#WV{P^^#X!qS^zt<$qd;a_P@28)Wdn)T{{r76GIh(e*X5RVKdC!4*YW{8C zP{ZYk-^Y7i+azwkU3*0?{prV#g+Fhexq9{L*RQ3!&TIAgi~1G(x_L&ttho5|hYuSH zD)!&MfA{Xv-!)w4(tgOdxH?6s3>{+G<95SiQPp#%ZvV%B+2n8ai`CM}XFk8K&f5G) zE8Sb;{qtZC{Qh|~{WV*<@f;r;28Oknv-UCnyXTYX`rNGvnEDw!UHx3vIVCg!00LAO A^8f$< literal 0 HcmV?d00001 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 GIT binary patch literal 1300 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST~P>fw4K$**Ty%$lXc7)79C`(9+CI z*GSKhfk9(p>BRHeE`cJ)?LRk9l<!Si7_rA>ic{T{BW{wMs=mon;%Cn^-+$#y#a{DE zDXVitRn6*je{kQ>%3j(rWsT07PTtGK_s(4LF?wG1?&qENd!LuQ-<iO#c6G|bH_ahd zi`ms`PW?Hw=uy>CPwuN*E{O*5=3aR}f7zm1+l0-JV*5i^f7)5UR!_(@WW!aC{uQ(4 z3C<Il7k55G>^+zH>dpX{A2B<kr*m`NkFMHrAYbe__n+jAay&hadjlgnrx)(%x!N*M zXxjA0o~qZcJgQwY{fhhL;>|+0kKQ)mFlgj&)jTG7*xUZFQT5BjhPcQ;#r^v8?GLYd zvdSns<zl3wUlY&WMe97jt~fH|MWCeTrm+A2zg_L%<r7(T{r7@bM@{Ul4s2!KnS1$< zo!QwtoEGyQYQ>(4w{2SJe{s9_tmguEW)~F3UOH4=u#;8SKgxsuscQQUTe0Tv#u*U} z%c57rYJFL=<NQly7V{^o^Ai3uSUufS{Zl-6OZr{2*?(69V}-N8BeIx*fm;}a85w5H zkzin8HurRK45^s&_O_u{N+d(u#osPGEC&yGTsYynqc5(hbLLEMfjdo&>kgz!oaN)< zadqT5BW1F}^Z2}n(r$6lt;K$62lxIFsVQhKva+!Fk<9Qv#j@9M+UhvQ*ICI|nG@!6 zvK)H0UVhHJdFztc*R-x)y_&zio+D+}vcj60KTDev=KhiDP#0rqbP(XcNBp?`zxhM; z-Fx@?+S}Xx=i1uX$k^BYnNca|su2I?-@iH?v2Oplwe|J$D|!DP6XtGmP~c!0Dn!n? z%9}+yWz5aZi{pO2wXwCW{q^SY<HtMp?VDG4_PdeqWeZEonP1P{WZt=Z_w3JJ{~mq) zdiA?>ox8%k2mux*M+MwO+F#T663K7gzdv95bo-}IpN#I^yJuHgmYtpb=Iz_FdtUsW zu`KhObvkcKa&mG^Ow5_er`wAwD=o#kk1jtaCo9|g`Sa(Q;(<0BhfrX00F&D)_CNP? z^7H%W&YkN&|7kYolWrhuZ57l1Mq*OhhvM%~i!A5OpC7)J_1Za;V-f4BSk`F!>u@kN lZUDx>@8YY<3BJ1jS++}hR;-+2Cj!h@44$rjF6*2UngA3==uQ9t literal 0 HcmV?d00001 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 GIT binary patch literal 5358 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 literal 0 HcmV?d00001 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 GIT binary patch literal 5138 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 G?0*1WO|Yi` literal 0 HcmV?d00001 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 GIT binary patch literal 5203 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 literal 0 HcmV?d00001 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 GIT binary patch literal 5020 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 Y>Xp_0qRxzf{T+y=s;)|jk}cu?0AeLb6#xJL literal 0 HcmV?d00001 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 GIT binary patch literal 3601 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 literal 0 HcmV?d00001 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 GIT binary patch literal 3848 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 Nu>V{ztu%Ix`9G0n5%2&2 literal 0 HcmV?d00001 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 GIT binary patch literal 3993 zcma)9XHZjJyA4v+(2*t`MCsCEP>}!u0un{Q0!Rx1L_;r9A{bhNp)=|W;ROvKN);iY zDhLs<P!d6+5HT2ms1S-EAmDd=znMGt-kCf1`?1fNGtWM|thJuCpPl7$#zAzS;yw@v zB<hHOy8`dQKTSjkc-CKPC<B4`a3Qv~F43+IHfoM`wh&_r2*kj|&=>?dQV?4(q6a@G zGhk&p7+!&pl`VQ)LYF@TdF8C=RMLMWZRm}u)!26Y(_@90+x28xf1_XupI2bLOhMu8 z4D%-c#9`7g^FoW(=#jZ(o@I5?h+q=59{uC0IQmI~fy|P{oW|<#2e}&Vy|Oq@@~wtk z!8AAbmXRb(P**I8qFerJLh<fQ#~$i&_Bp%pm0syyx7vhpjxsy$&;f6$Q_^uEQP%7e zZh0QtSECp+;YE~)LGkqq{=qA91IbN=43plKs6-R)fvOnQ)kDbT@87d&VatXwHUdj* zbYbK+*tm!A#<p#lVWg+aNxHT9mZNpCz#~vJC$O+`@Uv8`UCqEDZFt^mM03#k?{MEW z+P#UIkDRoU;J$cX#)r@&7=`|c0)Ls@`(O7OHh<ZkQU|9Ho}GV}DAv=&H+M16fVy9f zG`jiFo^s_}ja2JlsinAmLy}iIahZa(!BscTsHk-r`o@YT{Te+yr%bfke?GYD{-gPF zfRnuu2#;tGXutHI#s?}Xl?Rx6<_NcTk1t(dB{`3;sSYepSR3MW?*5&CQ|PDnV@$ja zAjM(BV!|TM^2gP#eC@TXqM8Ix1XAieDuhy8FeS(LA?ye3pak!_SdYLsqbyjOv!;y6 zV(GMM_nGeGTdM4(Q*+qv>D5Hcwtw2)a@+{?)lY1mj)}>W?%LXKiW2m|?XDXj4TAtR z_`coGI}SxJIoxQ8kkmf*Mz^o8EaKWVJ$+f?fEc{;kStL!G&J=0c8PcsK}KGrU=P2# zs~_+iI30n*N$4Dv(8!2PaX~z*@9aFNsi|pr1LNW%(c9abewJeiPB(16I<8v^jqI(; z8yXyhoj-qjDXR<rUqdY$=j7xV{@$^%@%2^xm@uc@xU*aI0)|CIMj96u6%~+3-)<we zwzl#M3sY${xYF^M!Oz{@dh&$4BZh|kvqL04(18O7RPtyF1HX9Pd2F^nIw<I?hLq3@ z|E@B==&<JIb)_?jS?M_WsrX1}f=1!;Oa#ML3ry>AkA$|HN}X3a<CzCU$KW#&Q4U(t zS$&SL3yw<gmpCh(DJ*c-7CT&T-+LMf?!OH!)z#Gn`8gG|N#To33)6(fQEOkv4a=?F z(P*?42)|lSp^RH^xx2e_HhxaHHiq-@@#U73C^lln0#K-3o<5ckDB-p_eBazNvF!y6 ze6P7#da^bA=ejXmR9w8`-aXLH&W?|zFu(E5hYyd8jg5UOxt1MG3~YX%-G>*~ghH9M zbAD<<{uIy1`B8OSI0{0dGZ?9DZP0Ml44izHqz2tp3B+%#Wa&VrKtVz%L}E6a7zqN? z9{-TZy6_n!Pq@!jmogaOTXL0VDJm&-eW&R6H)A)W+%H_vFg2C7wY61&zNcm2@fFXW zsT`Y%;1?1SYS`LMPEHm9gMakA+{NSO-7y&b50jHmZ)EFmxm;^3b~}AKWxVD*G>Stw zcs9o36iwrQi9=Axrm_e2e<<gl;A>XwS2kYNK0KV4{;{>OQGCSmXQ_6%_>&vI;x>Mr z{hr5;o$#KUmG;iX&5u1mlC*y$4<~)OY9RKhdqmhGc^mrb)hlhKtm<X;J2>PQdq$b7 zhey_l#kRg$BmJ6Q7}!ZX5*lJ5C4_<{-s*F#1?Wwq2LQZh1H2Q;K1VP-0X?W?OW=#{ zX!xrM`Bg$oTcq{^JTx^KBX>&!YBKS7V+dq++!PRoh^S}=D{R1^b!ujzg5Y@etV2RV z`v9*pUPlNe;ac$5P|%w)%gHZ!d0AP-D^G7yq=b85;J}(c0VT^sndsR!k(=Ptcl3)H zqRkYUKf!@<L}{eHPgne_;E1(`6;C3+pIWBbdxX~Kh!TvJXZD{@b2?&568{#iNx%As z<Ca>xnPF&YgKXBFKfJQNb)?SH*fCXz78PJbY;RRTyop%#X-&E<(0f^77~$&XW~uT= zwxy*?-ZZ`?oz_!_%*<pc6pD{!+W6Is@vCcJ6|~c)t^eUEb;GufOx7s1O7Tg&W7w3i z8kC-q(H?lh%1SkOxHOU*AZOsIg;nI4*xB2!UyqI5S>=V-*VjM3q;Dv0@8Ixx?2(%W zzC!QXwQGHA6TZBK0QuY(C~GgTNvrNzTYZiHUlIRPE;G28n3x`R#L@z<^{#KS$Cg|6 z_wSfD<yOAfyM_ZiwXm2^J$zFszjw%_moHy3Tf<eIwTtErbMkO)j^5tNkgzEce4jeC zI6wbpO^vRAfB+%1P?0|}EnpzmpVbjdZp@XDlf!p(92XE4CWbP`jV1hjvCD!C24i%3 z+K3v`bZK;Kj6<Ef_x852g!SRGW$8mfeHr`ZXpp(#zlpQ6vpP?Mgxhm*BHZua;V6{G zaG9kZRY651_rU}0kEPFv@`RgKyBquvlIX(10*a)ruKuvLcJ-Bk4L3!8P{z9}UQ$O# zXJeKWPJ{68+2eBld`95wJ6WNH*MKNkR8-y4Qdu=+yNUUryj*piH=S2msnvvxQs$4O zGnw*H(b4p&Dg6$w%a^n3>h$UV1kPDo^L@mwBI!fbWr_O)1qI0@GjlLaFz@7wh|i6+ z^Yo#5uhyC60aZCx<)Z+#q8~FvBh*BLH<^@>kZ?p_UxuZn07gr#FS3ZClhmxR2US)6 zBW0Fd8!i>{g91<2-gym<jL5MrH=zV|_4L@h5xFgfu8;{|c=rzE$&)8Xj~~~iw#RT? z$KJj3Su>}-?35ook6nKs+_tkmX$*xPp|(e`ZOkAL)a;vL6p5n(h6DMZ42y~~0j!<| zsUVX_r>3$!nu7ooejUx%gWlLOtGl2#{P&5&R0T;%zD`!|{QMOF1S27n-076x%2`=i zK%W-oEM>xnn3*Zm_cXJdZk&Jm$J&~@g@ppp<ta1B&rhwUwpLPA)rjg?_8hhw7!e_H z?OomT>hfe=DtB%U|Ky3uRN}XKU_};SPIyH{#o+L8KfS`i?)qsbr~Kk#oesycKG-fb z<gK>*#f!wS#SfIz+jt_!11!*ED{h%?nY2y_3!9{ho{0M{!O|L8-Gr5;>?U+A_3kH= z4ni<Ji>yw&V=qr&kLNQOuFJn)S65e8w6wU#6m<;*^FOMo>1AD?Ht1-N`r_2J+<)*d zNNCBtVM5+np(xIvB!|QC!(f~-tAJ4Kkx0?h)KnVe<u9yZ{@GKT0)QFID=JD&X6NL* zVE5sT1Ny;DNQO}PKF(x#x1E#IzTNHBvrR(@tFzC;TKg~%VrXI9hO?TovNFQi8GikG zEAzp7OeSD#T?wl+h@%%)BfmL#r1vU?+}!MjiSEx<xPrmJF_=20c1RwZpS!SN=jtkT zrtG8+RY6839XRKvV%{QAq)KF_7~0Pd6cQ2wP2O%}&bEZSb@B1Zy{&ig7BHm(B)_0Q zvd*hjjQ8|s8S}wQv`t8e&bvAfO{(LEph1|Dm@ts8gbyD)=`2>~^?<%iQ8KAAG!x9h z39Izr_=Vxeu779ZgjKOFF-0vS5xA49Z%pI4Rd?aXR##VF@E#zx{2s!boSbO9a8hw` zR$-wO9mx>y6L1CAki>g>G3G6>rMfzBa&qRT8C^?ZMsIp-Ed2zp%k9TZ-^NBfz^mrb zqb``D9}Yip*bO#*aB#3^Y3VU@mTX5H9UZ;NeOnD+cZz#Fzwz1DzE)P;!QHKIMomM2 z9+?Yqa5x+=mt-y^zdU7E0myhuOG^}~IcUfxI@**>CIiq;$0!UIn+E$)JeR*UcHase zF13&bYM@JP5%-ze-15ae7Ad<YF_44FTinA<L!UnR0ZD}7GOAD{G`M45Wp}sgqeqW6 zR$d#ufB*%<xpU`m2lOrglSa;K=l{F>nMo5rB{6AM?96Nq76)7mkjbSONkSjHXKU>n zeRNc<!|T!|LRZ&GI?_beTi{9HYuPSf6J%1J<+?mIjHTP**k1ui7rWvJ7$Y%ERz~I% zx4K-Gr4_)4QM0qNW41?E&j($&a3?+jeYr%mvaZf~TipfF1K>_0MDdVbv(3X*Kn?M2 zq7mQ6mYBRu#VFL&)JRB3s8JpJtJ`6@$|o`@*CtSCG#nGXJo5tZLt6xbz|`IpLJCIB z(DqhVRo(CI_QjZLYYQQfND*;yxk;_*`^7L%FE9V*_Yv$~!KNXL=-HmI)_}#Sb|7QD z%p?&PFUkQfk^F1nSkuts$4AY~&7Y&10BCs2iE025kKbdigon#bg7@yV2F`_<nHd0? zV+rQ@L^&gkLx%)?Q+6oKAQ%iJrgD;yo-WXYT=DkVkG^t6L_tAeW4<Df20>~|uP;pE zIUF<)4E5QI#$dpJWj>x<Aa?@50Db|WG(CG8w%9(W0@PKv!hqGeZBwn_D&icvgX#;t z#x9MHb^!5{%QR^+34RmLucNO|YaThJ*HKI&=_S{m(>R<bxbopzW7hPxg}M2Gq@Rl! zFVRx0Ls2UTHFI<Gwr>rcaHCU~pJ6Tj@m?O_W-&Ug!YhlNJPkd){eW$uNI-#M9U5wo znwItglGXqD9qiQa?dUq6r;7ai{3z0XS$$YQfToSDEf8&>h2pk1KIb8voNU6w8<|09 zv{ng;WSq2)X+mloXcYjY*4=ZJGBed4jUq`(N&=hx%PQp=Q=2|c5N5DgIY3=rdOX$% z6TS6=`>?TbAm`Y}BUDE-$(i2*EpZE;*q^NHWYXTfd#{9qSf4&koNp)tZ0^Q((qF;^ zVU2{5%ZEEzhRLiuAHQ%oM<v9UwpOhPE!qFRS^TG^Lh8nsMZn9**NyHkFJEvdW8X=d zT_QTzKgVDC?-!N-XgI^psT<Ez1-mZ)jeS=&5~%odQw`fIO;1@?mn}#wPn$L*G|XGs Y9~r66(dq96u6H0uyEE_x8^6?l0o&<x82|tP literal 0 HcmV?d00001 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 GIT binary patch literal 3887 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 literal 0 HcmV?d00001 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 GIT binary patch literal 6298 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 literal 0 HcmV?d00001 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 GIT binary patch literal 6457 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 literal 0 HcmV?d00001 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 GIT binary patch literal 1318 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST~P>fw3{u**Ty%$lXc7)79C`(9+CI z*GSKhfk9(p>BRHeE`cJ)?Tt_5FZK`<o4jGxMF~~2Q$A8&vyN#rpFLx2f8|fbUh_#g zQ`as!a%??oJ!5q0T9Jv9rnZ`HOsqOL+uU@L<@Vz9&+FdXF0Ve%T5Yj5<x3WS%BD(P zkq?&7l|G-lCBEwAf_Y_8i@MhJu{H;XY&l<@leVXH_ExR=zpqP8f3%Bfo7KZW*Q=in z2_FhxU3vA1?87ou(O4ln;j+vk$A{Lz{gHpNiY`6MpS-o;1pl|N#Pm<%$Llsux@2_d zQ&E-COVOnz@}E_GKfZa>b0qJhUWpUShleSX3jgpJ9_TpZyyN2~eQm8L)m=X?KT!#v z7@5l3n>uO9iN`{vuB(o#s=4%^_Sl^E?SFp!*CPwAue$f7c~<)2iubLzay4=!k6oXb zDAKp^6{p_jr+@A!^3-?i3%ypwcVHvq^YB#NZQSCsFZ^8LwlyPgmC$x|p$}zTX9Y8M zH@2~^PG4H<C#SGSFTRww`u7F(=d#`Q+@a}v7Os3T9~dW`1s;*b3=G`DAk4@xYmNj1 z1GBfMi(^Q|oVRxk^OPb*94;=pJ2UX)Y=tz=b9*(*9`;6@<IpV%YZ7>H=56P#pxd)v z3$AM9J9l!`lK)?s*8T8M%`%&RzFxNf_)UhIkB@ILAGq(MAi%=(@cDD;-+${Qj(?Qe zeBf&DV+$F+_WtbqASp-c5f4vum;9(-8@4)e^UXP1%Qi;v^!N7*`@0C9XwBVzJ8^H^ z{H<l%Z~NNt-Y-1Tq#(e-)QFRKka>?gT5tN{mnBx&Z<998sF2!cJp26f{e8zDzxm$# zx6Z!jamCr053-IG0)2A$_*P{xmPQ8w4q6d2{@A|c=jT8C`t|E8w-42S`t!%;&b@nn z+wMPU+_PuTpO5o0cP!byeY@xPVnQA>_<4EH_wV23%AaTD<>jf}&Hv;uZ{ECR_k`9R z6Xb4kP~c#pkns8U{OXpgS*AH=vo~MU6YGBX@nhkmP7S7=8{U>#uettu^R;(nyE~UZ zwC;-_5c@V~s}oN@EwY&Fm%cVuM@-oNaCzG2pMS1@teEq6-^Tmz+Y4pZTMEaVwdgzi zu;9mzGD2R*8QCOBeE9si7Z`vK|Ni|8O3StX-qdqGdH3#J(%}R5&w+#Rk3`!+e!d^# b{~2cQOK`hd#PkH1*BCrq{an^LB{Ts58(99Y literal 0 HcmV?d00001 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 GIT binary patch literal 1352 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST~P>fw4K$**Ty%$lXc7)79C`(9+CI z*GSKhfk9(p>BRHeE`cJ)?LRk9l<!Si7_rA>ic{T{BW{wMs=mon;%Cn^-+$#y#a{DE zDXVitRn6*je{kQ>%3j(rWsT07PTtGK_s(4LF?wG1?&qENd!LuQ-<iO#c6G|bH_ahd zi`ms`PW?Hw=uy>CPwuN*E{O*5=3aR}f7zm1+l0-JV*5i^f7)5UR!_(@WW!aC{uQ(4 z3C<Il7k55G>^+zH>dpX{A2B<kr*m`NkFMHrAYbe__n+jAay&hadjlgnrx)(%x!N*M zXxjA0o~qZcJgQwY{fhhL;>|+0kKQ)mFlgj&)jTG7*xUZFQT5BjhPcQ;#r^v8?GLYd zvdSns<zl3wUlY&WMe97jt~fH|MWCeTrm+A2zg_L%<r7(T{r7@bM@{Ul4s2!KnS1$< zo!QwtoEGyQYQ>(4w{2SJe{s9_tmguEW)~F3UOH4=u#;8SKgxsuscQQUTe0Tv#u*U} z%c57rYJFL=<NQly7V{^o^Ai3uSUufS{Zl-6OZr{2*?(69V}-N8BeIx*fm;}a85w5H zkzin8&h~V145^s&_O5MKbfiSX!`Jqi20IGG4NkOjJ60{f#J8s><X-#jD=Y7uW4g0z zrcz|1qW6qrXCH9L2zo4&la5n=)c533<tgUpA58UYj=J8Oc{Z*7@1LKa3mE48Do(eW z`!8Z`nDy;>EOE0hU%ov1ZZ1FL`k4+2Z|rR={{Q=1P*AX;H!@-MyLa!}&d<01|NKp) z!t>|P50_Zoy%F#s$_5Cyk94s*DhRMJQHOYV^MCV)|GW0?mEBkWf8Vvcn>TMxw42{= zBj~CiT~=IdtTX-e)^~9+F&rs}*F+GowdU;Z#K|X9X85RO-@SYLwsfUboYDl|-ya?x z-sb+mJYu5)2TPL!t%yS}>X~b6Yi++x{^0%W^Un<tI&QVaUmWaef0=-U%F4^#`3VMH z;+OT6{(gSP>V)b}e*F0HMSh>$iK|zy-ta$kjXKUh^uj*Q_xF#Fk3anQvE$m^^7r?6 zyN@P4>C|Alxj{dEAJ6HhMQe9&zM1nRv!YX!n?Pu-uQY%2_V)I}+w<>V+x7d|+1V49 zKU4)8bo%t^-2Mak8<HFaI9M7TXiVIwuPUgoxA#$-oO^~T`s>%%*VEIjFTeb9JZtN% zn}=*ZteQJ--aT;W9r}0i;zj%R_V)UmYJrN@fB(b}NqO_+KHMP*%!>@3u6{1-oD!M< DcE=DR literal 0 HcmV?d00001 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 GIT binary patch literal 1951 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 II0mNu8zMAYN&o-= literal 0 HcmV?d00001 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 GIT binary patch literal 2093 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 literal 0 HcmV?d00001 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 GIT binary patch literal 5822 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 Q3=|?Ktt?e0@h;^50K?!I<^TWy literal 0 HcmV?d00001 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 GIT binary patch literal 5639 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 literal 0 HcmV?d00001 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 GIT binary patch literal 3675 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 B4W$48 literal 0 HcmV?d00001 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 GIT binary patch literal 3595 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 Xe<H|e;pqhMJHcgZ1-3+A^H2RhKfJ_S literal 0 HcmV?d00001 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 GIT binary patch literal 2222 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 literal 0 HcmV?d00001 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 GIT binary patch literal 2383 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 Rm;qNZgyiOh=eUGj|2Kca8$tj8 literal 0 HcmV?d00001 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 GIT binary patch literal 1962 zcmb7^dsvcp8^_Vf8<tH#&BH7awak&_A&(;*qM)yuPRq=hnx_oQ;b|x>FVYGnHSlF^ zCR(PMIYlj{%)}9b(<wq8l1>xPCZ%Q=inWK{Ki>bgy?@-l=f1A{`F`)uef^&2E(i|7 zZ{2LR83Y1t^&{ZE0iHdYfcy(M8;&$qfIwQ*WN+`_^l$K3grASMi-Wt1%N|F22N1}X zk;xE$i3>3lVVrtWZxFU^D{U&1nAo`V2AKJm32pQGe|N$RsmTvrnJKJ>DuIv#&ee)c zXfR|jS?BjR$L0)Nb=}W&=cbEi&b~p{Qp8}2TSNTHN&Wbi>^+8y?lZ`@12W^=bJr@e zj#d>m7K8Ib!|sYHDTy7KS6l5W*GJ8+$lJBsJjO$OhL`yU{LuRB;mPgeSvE#d@B;=} z$!TAYdxn-os86Oz<*B8u-}UG#5631g8;j01Go_CF<+L2fIis2ki?=p`OA8B^1gDqm zGq5^~<MB+ATF;@2_Ph7JCF$NT?ItOOs>hSo;f#octwjmU>YfRBrq6AW%}!iNAK^}- z@?&cBJHfTl+b<^b%90+D-<+47vQ04+jxu5mi?7e?+TWQ{KeN=!rF9+}$@#piS!*Ud zVNcr@<Ew+JTfVI)LT<ykyWopioBLrW9#AiU>yv7J2-<<@u#e8%N?9M=HM5<L*>WhU z<9gj}C2%LY-w`6xL7*)Ln$QB3m79P-V6h+W>o9V;;yH#G_SCFvd^Ci9#206%_p9%# zE-wV)gyoUVs|JvfZRc1n=PqM;RF)$77lA#JrsdPLtbf=JmokJc(EY7k?l{wnE_HRH zjKsIBp51XIFlO%yByWruBMv~JP--I>y07KKLl3P50^%|hV*SCWXL&hoB?sMia;lv) z;kyH}h|#9zdm${L1c(m&FH+L2l?%^LyB_O26Oj{X(p&TID{{#Ve77=v(?61js7b3T zm8!Sofdx9pxiRX&VKQ=ldVuY@w7&Yre{?`BHfGn55$WmaEDoE^E{f!yHY&g1(N0-? z-Ki^pZTTy-zhw_mQ<`JGeaXwMR6;&NA?pLw&o<pe^D*YlpXSbQ{)NL4$z-y-qMuBt z!DDM{Ip21kaR=axD>1nhN;d;bsNI}s>HX5g`^9nZa!hU~g8|8BY?$JsL#<#CD^o*! zv;mNK_sM68N3FGEZ666Fqtyf*exq-}4bE=h<uI1pS{by!>xsmTv@(o_Q}+cIxUJ2b z7kHV>uDyHZBcH7&0>W!oT~mcWJ0%sqjt~KIuU$dqG!~0Bf=5ELtl`uqdYdL2o(5{c zGBZGY^a~9@awMRQKUIiBgh_uS=wK60(Qt@MHFH4Y=Kbz)YOuiC%AKN2=^!sv&2~P5 zL%IX(n80j8sZ<(<gh6})?U?=j{dG9$txC+jv?>2H>h~mB926!h-cCPQd7zsU%_~&g zzklD9PNz4wx0^jb&3)4OvoCmAt$IsdS1Rs`V$QX3xpB$KyDQL?wp|Rj;~}1u>}POD z>)mjGLMEdFSPg+501Kt`5)R?auLk-SwY0V#%D_+uAs6*6ifa#63gz<Sk{`fWMW9_V zgc_V)ddUil7<y7YL4!i}w;GdhkbE;)y&KX@>72@aS~MXlIY`k%9*>K&I>tL2wL)5@ z(SmC4B$05=sLPx5cLY#$kn+AOdr9N`^K#O{6#Ir4Mz2~Ub!5N2IotV|2ACT<*SQs& zyF0P~(=5U9oSjCZxyQ9}2?;jS)6<2oJyG97g(E|5NFDx9W#;&NFB9*_8W^Ki%VJ2X zv@V88`YL<bI*9FYDB~sRUfw>YOE=6C>O0t`kzAIUXe8K3jpV_JbaR-tr9`Qzd@fju z2Cn-lH<fz;n6xPm_|Ut%>Be+Lxad<oF)>j-{^+6)vW}=xg&>(8BEUGyA;>zj1mM=Y z?r$Ba89*=jKZG*4rp>SW7|y!^vwc-4=1mQSor$5J!6(`cQ@_v&+9D%TI9M_XG$k!h z{+3+g1k;X1IXO8w0HJW$Dg57Wreu$ejm>6wuttD&A&KblOL{r?E=Tg}-A%MF?p~gk z|EF!DqpeHz!v24$?T1zjkw|25HkKyv&v?1KKb(goLZR;zQKb6#MKu}m<|U7FMls5X zo|qr52iIOd_2MBVJOhJA{mp{<?-uB`_&z!+sz{+w^p0A5MQ+l#kBl6iB6_l0II8Z? z+%tL(a-1qQR*-Wv@IRrKck%iBXwI>RYvRgW)J6b$sQY8?kI8>rbQP+)44exh!PYw3 a1w1X(e7Qe%p^yXYdyt<`5Uvp$bM8-`B2lgY literal 0 HcmV?d00001 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 GIT binary patch literal 2089 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 O8xR5KhHbD9rv3*~kZ}zF literal 0 HcmV?d00001 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 GIT binary patch literal 2994 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 JYV!M){{jm#Vr&2a literal 0 HcmV?d00001 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 GIT binary patch literal 2942 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 a$N#=zXF@_6r55Sn_(&!egp!laQU3x;by|M_ literal 0 HcmV?d00001 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 GIT binary patch literal 1155 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST~P>fw3{u**Ty%$lXc7)79C`(9+CI z*GSKhfk9(p>BRHeE`cJ)?Tt_5FZK`<o4jGxMF~~2Q$A8&vyN#rpFLx2f8|fbUh_#g zQ`as!a%??oJ!5q0T9Jv9rnZ`HOsqOL+uU@L<@Vz9&+FdXF0Ve%T5Yj5<x3WS%BD(P zkq?&7l|G-lCBEwAf_Y_8i@MhJu{H;XY&l<@leVXH_ExR=zpqP8f3%Bfo7KZW*Q=in z2_FhxU3vA1?87ou(O4ln;j+vk$A{Lz{gHpNiY`6MpS-o;1pl|N#Pm<%$Llsux@2_d zQ&E-COVOnz@}E_GKfZa>b0qJhUWpUShleSX3jgpJ9_TpZyyN2~eQm8L)m=X?KT!#v z7@5l3n>uO9iN`{vuB(o#s=4%^_Sl^E?SFp!*CPwAue$f7c~<)2iubLzay4=!k6oXb zDAKp^6{p_jr+@A!^3-?i3%ypwcVHvq^YB#NZQSCsFZ^8LwlyPgmC$x|p$}zTX9Y8M zH@2~^PG4H<C#SGSFTRww`u7F(=d#`Q+@a}v7Os3T9~dW`1s;*b3=G`DAk4@xYmNj1 z1Jepm7srr_Id5+n_C0bCa0ry1sBrp`*OHhS%mpeb0)jbPgbglTXSr4tR;6Fe?#f#8 z;=p4Q-}S%$+VLGQ-@R76;l7W801H#%|H^{hciSrGZNI^4W`6$j2A$K_H`fbrFf}R+ zN8<NZ_Ic-@Km1v<&Q`4MNzqQ8_QQ!=D_64@?Tu?c{&?fJh#wQLzux-FCR~R=2&~?J zw%Tg$o_FWc%66C5e?Fc$EAIR6+CSBhzzqK@@FV`rXB(Z<pT557Vs!+C^l&4-&1ZlA zy}Dxm{pdGLvw#12SI_;V!sg%R)Cb#(k2V1l&+bB*?e`e#*4kg6Y%t+(cM}gVAv1Wo L`njxgN@xNA>8gQW literal 0 HcmV?d00001 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 GIT binary patch literal 1208 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST~P>fw4K$**Ty%$lXc7)79C`(9+CI z*GSKhfk9(p>BRHeE`cJ)?LRk9l<!Si7_rA>ic{T{BW{wMs=mon;%Cn^-+$#y#a{DE zDXVitRn6*je{kQ>%3j(rWsT07PTtGK_s(4LF?wG1?&qENd!LuQ-<iO#c6G|bH_ahd zi`ms`PW?Hw=uy>CPwuN*E{O*5=3aR}f7zm1+l0-JV*5i^f7)5UR!_(@WW!aC{uQ(4 z3C<Il7k55G>^+zH>dpX{A2B<kr*m`NkFMHrAYbe__n+jAay&hadjlgnrx)(%x!N*M zXxjA0o~qZcJgQwY{fhhL;>|+0kKQ)mFlgj&)jTG7*xUZFQT5BjhPcQ;#r^v8?GLYd zvdSns<zl3wUlY&WMe97jt~fH|MWCeTrm+A2zg_L%<r7(T{r7@bM@{Ul4s2!KnS1$< zo!QwtoEGyQYQ>(4w{2SJe{s9_tmguEW)~F3UOH4=u#;8SKgxsuscQQUTe0Tv#u*U} z%c57rYJFL=<NQly7V{^o^Ai3uSUufS{Zl-6OZr{2*?(69V}-N8BeIx*fm;}a85w5H zkzin8y6x%W7*a9k?JYwul|T`Pi>nr{Nfa_P+YlU{aOrfRZr<X}kqzDw0zI)uS=@Nt zdT$7?E_k_M#;XT&KCt#rkNI&>K0hz-pG^Ppc;*i$E&BE!ubgMk*M4}%H^YKFv)N}K z_iycQC{`2TxP1NK-@ktg3JW(L)7`LY`t<1!fByV=`AxJ!d3pI^oAdwQYFLQ>{`)Ul zVP1p)3zMV5a3d0<if(?akSW`JcarwQl<nKMA1>P+Tk?ESBhO(28(Ul5y+VFQzLy_0 zRwOwJaIiEw;3Ojcn7{w}_3I_=hbrOW;oa|JOePe{v;%=^_@Pxc8x=TMnjD53ap+6w zhhM*}&V1ejN<6^8%iml$?|J{>hX(5k_~&droA&udCo3?`EIWPr^xxmVf1lsV-)Q{h ayZqnxMoJ4FK6?X9`wX70elF{r5}E)^$E%zG literal 0 HcmV?d00001 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 GIT binary patch literal 4369 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 Kd4=i4oBsnq$UU0? literal 0 HcmV?d00001 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 GIT binary patch literal 4304 zcmb7HXH*kPw+#Y<0Rkve1Wo8wiqfS72$)a}U8O3ZSCJY8iIh+@NG}Q+%B3h>K|lee zN>hkP0EG*o3kGS@OMq|izW4opZ>^NIX6DSCIdk^j=R{vJzsLbS1%*H$92hjp68v`_ zep!!#-!l7GSr7<A#9symm;5a+>LW0Q2C7OLs;Xzt$tyu1Qb~7{dSp>po(|pBh$ML2 zwH*5o%BlJx{jEj<$HdL9r#h3KsWtX@2iB`m+)6x8imQg4-C`S*x&4BeS}Io>&C%yS z+TCw@ZuZ{d_Q+EGEN-dRg;Qc6bz(lkS~HJD;=yNnOYUS)8-{J5igMDD<4*rl_RHL0 zZI{YP#{`>_mD<oU`Qs|q<pZn<h0n9Nvv{(cdT#PALDfp4FQK*^YObcSLC3BJ)iinY zcCgW}SDaywCDdc>qtkqAVh19#jz3MC!iazGpA0Td>%Z9NUwM4EZCH_2k-!-x-7fIa zMEB!4Jk2%C=`vQhQ6XFRWASwHx!Z2Nmxay4*{Qj&Fzbb_=|8aom^Zcu2O9%K4j51I z_uaqe+LU!Qo2nUW@AT1iRLLX9)UzgJ96vj2hv!)IzB73~%QZss@pYuM;C*VD&stnT zbNRHMxsv3dTceb~#Zg^n_7r8PY?Q6{zN5-*nnh~|$E_-#I`svA^Q(q0L8m192W z%l;4u+lj+31LS!IKLi3>#h~=812Yz=m`lTV;T^v%?32q?5uFX*Z+M_k`@fGE{ME*4 z`}iM$`hJ0qN-KrYwaHHc3THB4=A1!Am!P<ERwDC5!z5$2oU<lHsfKUgFtHxX(R%dd z<Ild?Pg7H?X8T=ByLCT(wx(xemxRR*LUW^|qqnbxhU&s&D|CGW0^qBI!^0)%+1Uwc zJBqQN%+P3bEUEvIi|H42u6Cj2t?zLDI3tvyA;y=tU97sQD)QmOhq`<7MMz64tE8-~ z#Ww~@5`2AqeHP3u78U&dVT`Wb46sNgmP6}t<o|VmP#_<W_P(s_Y*keiIyknmjY>To zRDZ_F$?5B_^}`Ph_voOXhWmP}`q2R{ARuk0v&G5*p-SQ?D=V{ve-saoh)BxFh#efX zN1;#%Wo4eD5e%o|jOOx=8Tb;2ZujpCbai$8Tv*ViUq+)@v+#==t?lh1T3SLmIXTOl zn`z6bbL|AF?#W3JUOv9wfq}bQm7SevH+T1m?b+;$o}O|C4~1I^g-!GGavnEs^in9{ z)6;G|e0-wk&!1>$Xt0Ev{Z{9CA^?YXY;CEp1!)@_8z-ivbu`-Sa|#iS7Zw+DtE*1} zU(v?K`o6wO@ux2wg~Q=R2`{CDh!6=02}LC(LQ@m6zrP=$ppf9KzaO&|r%%udT4Uu? zaB7{+4vnYL{whkyW2FnCkP<1!z{keMM)6b}h=?T|^L9&(*z}x4`rXFo?BecjIG5MS z%OX;fl*IjDhgP8Y%+wNY_*9+iVRg02gN>mZTeO1ge4WShK8X3RY0e?r1zlJ8Sz7iD z@7y_C_x}A0orj)aMmkO2v1NI=yuTxnImdZM`~Hd;xRt|-{k`|6LEvpmPO6d?mdDC$ zW$y(9xT=zrw6)`kiloyrW*dvj%Gxf%gCmJV)}jOv=^V!2AjH%I2yYBCcn13`S6b+F zEe{V5pUp)_PjBz{YeK`r!-c>oij6O_;V1Sda=Npti><e}mlz+fLmwI)J+nxC|ERfH zZ*6_O+3Vf=X*td_T21f9LSj~Dd)V+5qAa<PtgNig&Q7F}(b3|P5~XwJ!beA)M(Xk3 zX$jQ6zKD?#2O;gCep-U~MImB~kcJ<Vp`oFaoLuYP?xyedio2<)srtH-f^$P`Ybz@7 z{(X&g-)W+Ao268oQAA`UlS7RUgNllZd2nfMtp*N<18b)rGh)><%~|q#i3z&<{1^<z z9G@{W<55~xrg6vl6-WnSbaeA0cnFa~u`HM$UbqLG*KofIfOWF>R<(M-qL81TU&fn* zz0I-fw{Lez5Bs$d3S(_c+O}6`lhV`C4R=6L)z#4rvNwc?(CwX_NwD%FNZ;+ZH!Ui` z{!l2i1;12(vEhP-M)bgd&5C2~B+^<=SeT(B0WLXqk`{6ghWhcjtLu}R<8N4lgAYQ6 zkB{#Wo&JDA&dCu@bL5mzfuSG;=9wbYtIp0HBO^kzmkz#&BgBLR$;Lq+Kfh$01-OV_ zenCOXVqAQD^5UnHEF#48bbd<x#DuHxsLd>riCaJb(;(f_79&JNJO~a>dj8zFuC}t$ zpiUA*8d=C#VK0}D_iH7vipj`4#`O%q#e|t*C`A9eZa;*n4ZsTROylD=>nKgG$|2ML zH5wWmL{L*i!8*;%cwE95i)(RiA8mX>Zxegd#Kc5hT^%v}XZlp!(b3USH@t|$$`=Bw zTWBbpL?XH2_4M?L54xRzy=}=8+)`(f=)XR+dQ>JYZf|cVEq>x=5#gE}L+I$}{5uuN zxIG5$o&$?qIjIB!$)q2Azm9c`uLW%D>Po=%xF{z6kCpdV4o*(VKmKvy@pynQH#`Fa z11l6NGMpj{LvaBY^EVR<3JN}w&Grs<3lAC-86Y#WvzX1vsVU4RNFDCh?rwIP6AHyd z%Q_7CAeGwI)C8e<*xU2}J5m$8t7>a&+qpsM>x-&^v5h0Gmwr!mOpp2QQ%n@V1q4`f z{Z5=Xk)EE87+zgn6+Org)R;nAzbYw-!M(1kvL>3C?ng6XD*!0hHaAneW@cu<1D<Bd zwG<Q-+?p$SjaEB<{(R<&^l!g*Lg7~cvWESg+bc0AVq#)2o2g-p*qo(?U6_G^K|X0{ zXsGk^XA!FP5g5uHNWj;@L3CZwz^&giB<~g~H4|rd?5B=lEatP<yS%b8j=Oj7P7edT zuySySkJ$hl=5OD=EtA#8#%IW+>0y8sDQRipF&iihHPPPQ?n9%9Q%o@XKSyf)m)F*C zpWhz$K|panK4D`ayBvFadpB?2_Wx(<!`{Yli^I45Y%qDP`ihdK=3_xMFZDhA=aP~V ztgY=#f5#<o){LjBAYk#s=`UZ*3$&l?<k>mhXEw^iu4ljBwevxoyY_Cl;?0}zs3>Nw z2U}cLR#yCig8sCE!n7w(a_Z`?5QqOdfaWk>zI?g49={0~Z+CaM{7M;kqNsseQEk_n z7Z>H_c-ijVyH{Ra9rfuG2Ghu2SzgZNUBCV`aVdZ<sn0^8QsWjEy^0cq*>3b?wbA+t zqC(o{#<(@PjxrrRI!O%}GYkr<>%W*J4nw(s;0DfS25|B7ORZzwVi>u3bYJY;R_AKw z;o<o^H&?%AS94z;WEF6zu(TAVuOHrj5z7Ohot&I}$;KwCs!BP;d&(OmNO55yE68rN znc0s`l|$)yEG90V0|uWxd#3*&;E^nd-0Z?a+{%id+l?EdnwrmFXNv&gbHL%0zhr2L zt}9u`9Ne<10VG%Y`n5?g`@kfU=ZmN6)Xw@ZOSnNbpf2zfAJ^8ZK&ZuY4vtDoNg<t_ zvcwXoy}f!tcH>WE?UtKa;L9d;t&uP>Wo7F(oCd3#o01%@Zs#U|)aZqT=nP3r71$?7 z$HW)_nx%OJ2W!85`xdQc*g-%S=H?y&fONyl%F5pK_LdyJE=0^fa^%P*M@O@|p^=f; zp`ptZvm{@{aalPzl&L8@&ExuY(KEUsrghRXFw`~8Oc)5aI92G>sg|;<@4%*Qgg=t7 z-9=tsUoQt#L=u@qx*_v=$E&|PiBdDNvpeDh)xg45LMJLPP`$Xg7_A2IQBhTes*^qi zLm8Q8X<>Gbi2}(ZlgWRGhlhhjL^LEMWID8bb7N!jOXdZz>jctGLP3F3T3R}vba>Q| zXFe%+9O>5n@nZy?{1M3a+Qvq**W+-;q#RH?th{z;@cRJ;umEQ_5jy)zUN=DNoh;tI ztFMoH`jq#LHWno6Ngf{5{%9wsE<FsS8O5x63h7o_Q^W7->Y7jD;NXzo(?9b9jCBLG z1aQkI1uTv<_vjv&))jdO5xY-cxED)*)v>b^<mu&=>{SfN+uzH}D{yc9J4WsMw{ICc z>%F&H2vSH8NvDu)cM2Jt3?Pw+vx-yaic$~WQKr-BARebqZM7F4gfn7UBbt%>u-)Zl z#gM&qJK)d+k|+P~zahT9zWF48`oW<gVXE~B7)r0|%GvFK20$f0=jX-#47pM$6d(GQ zFNOT_r38I{cPXEAtC=7bYm}#FV{=kV>!%J+AE1w-qG<2Z9W`|>W+(@Tp}V^{Z3Ymd zsl*A|%TpkIZES5Hj*q)g$aQs^_GPvdGjM;N&mA3)=<Z|Bo(Zh2ttEQN0Fra5t*u3y zn21t81g*>1+Sy^$p2RX@6$1hSR(@3h==v=Dqe3wg2gdSBO587AyvQdxIy&;h;gVEq zkV?$4MxwNZ+NKnfN$|#KwI@VIEa67h{fBhUjp*oO?Ck8|7;cu_<C5kXNH;gJKY6UG zstQ&<WW*i=FBy{gZDvN|%GImjWH6M}-EBHOJ)P(Ul6|r{f+e41+(IZMl1PF8cm@r_ zU%!U!?FFwmiUSx+$jCsA@{(}=Zf;C}=-0Rz5Ok4Mfr}zPTJ6`bUlsNBW_3kOhh@R; zZc5|)`wt&t<KtoAKEQeo6oIH~*F^qc<4|<ee#A3kr7bKhR>nk{QqZWn3Q)RK^!Hoy z@bV^m!C<h-jfLTS(xD1C9LD?b+JYsZssol_Wo=?2*UF|5kdLne1BP|dcA#W6HZl1< zU)huFrK+s#a{vA_oK?I!7egQ(pAs4uPWCu4*$!gJrJ<n#N-_j>o<_s&*_rwy!hPBg zH&<Y>S&g#+)d5gLtdo;@ora1^R6~Of#Y|%Y$z+AaGJxWkVx|(>rpzLe9u@{!DY)WM zDe(QZvm`Z@4TeI3+6`2O5>!w;c7Os7_!-8?ooHSl+NjMJxMFQ`N6pq35w01uCgl`3 z!Av1nR4B2-V98#_ag5xlMtQ!U7ivMWDw4?4?=8WJyxiP+`zp|cGurxi`~}*}RKUGY z($Yi`h%nshKh?L>Gc(1hsZbUXbzNQE0(x`E`kal;1K?asB50fdnki(Gn)l?lva9(d zOVIUbX=!En;k_IkPXn3QekJRRV230oCx@SqG*9%Bmy;XsfQO1vaVoIL)5WEwk+vn` zRL@(tT0r+fdkGfLU-I!$+{OdSG3#|!A`{AW0jua876u-maRYjZR&$Knn38vBB9r6h z^)JkMyIV3UC@6gR-7a|3(^HJPFh7qnHim9Xma3E9n6ovxD2}IC=p^m%7eCF+jGLeL zJk%vRPt5<fP5W<W7dYp)NhX_>zItW+r!Q=1*zfPjf=*Ai#mH;0UpH?0Z!gHn$(g_$ n(aa^kuE(DuXkpPacEB(YuQ2*giXA8Di6Iz6bJQz+m+=1pNF*q; literal 0 HcmV?d00001 diff --git a/b_asic/GUI/port_button.py b/b_asic/GUI/port_button.py new file mode 100644 index 00000000..a856b618 --- /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 00000000..3c690517 --- /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 00000000..1b70c2c2 --- /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 00000000..1cc87939 --- /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 00000000..f787e1ce --- /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 00000000..5234c654 --- /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 7e40ad52..e69f0498 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 8902b169..c822fe8f 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 1987d449..8a0b4a9a 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 8da6a9d4..00000000 --- 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 5578e3c4..2ce78396 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 c22053df..25197891 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 be55a123..00000000 --- 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 00000000..222e4741 --- /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 e5068cdc..25f18eb6 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 64c25948..747b25fc 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 9c08aecc..1c0f4870 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 50adaa52..5fece8d7 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 00000000..dc84f0bc --- /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 00000000..746d1efd --- /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 00000000..09265729 --- /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 00000000..9153eb5b --- /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 00000000..8a11aaac --- /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 00000000..a9738a0a --- /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 00000000..344eacc1 --- /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 00000000..4c3763c8 --- /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 00000000..f0678824 --- /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 00000000..4d6ff833 --- /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 00000000..38e2771b --- /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 00000000..1f7a6519 --- /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 00000000..88fb087e --- /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 GIT binary patch literal 10071 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 literal 0 HcmV?d00001 diff --git a/setup.py b/setup.py index 43d55d40..e5860344 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 GIT binary patch literal 40475 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 P0Y4hIb#9fRY{LE@k7LaA literal 0 HcmV?d00001 diff --git a/src/algorithm.h b/src/algorithm.h new file mode 100644 index 00000000..c86275d1 --- /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 00000000..a11aa057 --- /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 75a77ef5..f5c4be53 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 00000000..9cb5b42f --- /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 00000000..33280f60 --- /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 00000000..aefa3a4e --- /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 00000000..7fa7ac67 --- /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 00000000..883f4c58 --- /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 00000000..5ebbb95d --- /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 00000000..d650c651 --- /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 00000000..c14fa192 --- /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 00000000..2174c571 --- /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 00000000..3af24c10 --- /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 00000000..c1a36cbc --- /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 00000000..2ad454e1 --- /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 64f39843..63cf5ce1 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 df3fcac3..695979c6 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 4019b3a2..4cce4f69 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 7b13c978..4dba99e2 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 00000000..a2c25ec9 --- /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 626a2dc3..00000000 --- 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 b176b2a6..6a0493c6 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 00000000..acdea48d --- /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 b14597ea..72c923b6 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 a4324069..f4668938 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 6c37e30b..f4af81b5 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 deed7a1e..7cc250ee 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 00000000..78a713a9 --- /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 00000000..2124daa9 --- /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 ab07eb77..42086d4d 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 00000000..8ca19e47 --- /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])) -- GitLab