From 7b263393b2a51b24f16f31fa02302c58cf6e72fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20H=C3=A4rnqvist?= <ivarhar@outlook.com> Date: Wed, 19 Feb 2020 21:49:22 +0100 Subject: [PATCH 01/50] add gitignore --- .gitignore | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100755 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100755 index 00000000..bf81ea19 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +.vs/ +.vscode/ +build*/ +bin*/ +logs/ +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 \ No newline at end of file -- GitLab From 1b46d8f122474d2e110b19f23c27bbd7641778df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20H=C3=A4rnqvist?= <ivarhar@outlook.com> Date: Thu, 20 Feb 2020 20:46:36 +0100 Subject: [PATCH 02/50] add readme, logo, CMakeLists, setup boilerplate and example code --- .gitignore | 4 ++- .gitlab-ci.yml | 10 ------ CMakeLists.txt | 77 ++++++++++++++++++++++++++++++++++++++++++ LICENSE | 0 MANIFEST.in | 3 ++ README.md | 82 +++++++++++++++++++++++++++++++++++++++++++-- b_asic/__init__.py | 6 ++++ build.sh | 5 --- helloworld.py | 1 - logo.png | Bin 0 -> 70697 bytes setup.py | 75 +++++++++++++++++++++++++++++++++++++++++ src/main.cpp | 21 ++++++++++++ testbuild.py | 1 - 13 files changed, 264 insertions(+), 21 deletions(-) delete mode 100644 .gitlab-ci.yml create mode 100755 CMakeLists.txt mode change 100644 => 100755 LICENSE create mode 100755 MANIFEST.in mode change 100644 => 100755 README.md create mode 100755 b_asic/__init__.py delete mode 100644 build.sh delete mode 100644 helloworld.py create mode 100755 logo.png create mode 100755 setup.py create mode 100755 src/main.cpp delete mode 100644 testbuild.py diff --git a/.gitignore b/.gitignore index bf81ea19..473e0e34 100755 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ build*/ bin*/ logs/ +dist/ CMakeLists.txt.user* *.autosave *.creator @@ -25,4 +26,5 @@ ehthumbs.db ehthumbs_vista.db $RECYCLE.BIN/ *.stackdump -[Dd]esktop.ini \ No newline at end of file +[Dd]esktop.ini +*.egg-info \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 6c51653c..00000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,10 +0,0 @@ -stages: - - build - -PythonBuild: - stage: build - artifacts: - untracked: true - script: - - apt-get update && apt-get install python3 -y - - bash build.sh \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100755 index 00000000..6b6dafb3 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,77 @@ +cmake_minimum_required(VERSION 3.8) + +project( + "B-ASIC" + VERSION 0.0.1 + DESCRIPTION "Better ASIC Toolbox for python3" + LANGUAGES C CXX +) + +find_package(fmt 6.1.2 REQUIRED) +find_package(pybind11 CONFIG REQUIRED) + +set(LIBRARY_NAME "b_asic") +set(TARGET_NAME "_${LIBRARY_NAME}") +if(NOT CMAKE_LIBRARY_OUTPUT_DIRECTORY) + include(GNUInstallDirs) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_INSTALL_LIBDIR}") +endif() + +add_library( + "${TARGET_NAME}" MODULE + "${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp" +) +add_library( + "${TARGET_NAME}:${TARGET_NAME}" + ALIAS "${TARGET_NAME}" +) + +set_target_properties( + "${TARGET_NAME}" + PROPERTIES + PREFIX "" +) + +target_include_directories( + "${TARGET_NAME}" + PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/src" +) + +target_compile_features( + "${TARGET_NAME}" + PRIVATE + cxx_std_17 +) +target_compile_options( + "${TARGET_NAME}" + PRIVATE + $<$<OR:$<CXX_COMPILER_ID:GNU>,$<CXX_COMPILER_ID:Clang>>: + -W -Wall -Wextra -Werror -Wno-psabi -fvisibility=hidden + $<$<CONFIG:Debug>:-g> + $<$<NOT:$<CONFIG:Debug>>:-O3> + > + $<$<CXX_COMPILER_ID:MSVC>: + /W3 /WX /permissive- /utf-8 + $<$<CONFIG:Debug>:/Od> + $<$<NOT:$<CONFIG:Debug>>:/Ot> + > +) + +target_link_libraries( + "${TARGET_NAME}" + PRIVATE + fmt::fmt-header-only + pybind11::module +) + +add_custom_target( + copy_python_files ALL + COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_LIST_DIR}/${LIBRARY_NAME}" "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}" + COMMENT "Copying python files to ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}" +) +add_custom_target( + copy_misc_files ALL + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_LIST_DIR}/README.md" "${CMAKE_CURRENT_LIST_DIR}/LICENSE" "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}" + COMMENT "Copying misc. files to ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}" +) \ No newline at end of file diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100755 index 00000000..89a500ee --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include README.md +include LICENSE +recursive-include src *.cpp *.h diff --git a/README.md b/README.md old mode 100644 new mode 100755 index f126999b..78df523e --- a/README.md +++ b/README.md @@ -1,3 +1,79 @@ -<img src="https://files.slack.com/files-pri/TSHPRJY83-FTTRW9MQ8/b-asic-logo-opaque.png" width="318" height="100"> -<br> -<h3>The leading company in circuit design<h3> \ No newline at end of file +<img src="logo.png" width="278" height="100"> + +# B-ASIC - Better ASIC Toolbox +B-ASIC is an ASIC toolbox for Python 3 that simplifies circuit design and optimization. + +## Prerequisites +The following packages are required in order to build the library: +* cmake 3.8+ + * gcc 7+/clang 7+/msvc 16+ + * fmtlib 6.1.2+ + * pybind11 2.3.0+ +* python 3.6+ + * setuptools + * wheel + * pybind11 + +## Development +How to build and debug the library during development. + +### Using CMake directly +How to build using CMake. + +#### Configuring +In `B-ASIC`: +``` +mkdir build +cd build +cmake .. +``` + +#### Building (Debug) +In `B-ASIC/build`: +``` +cmake --build . +``` +The output gets written to `B-ASIC/build/lib`. + +#### Building (Release) +In `B-ASIC/build`: +``` +cmake --build . --config Release +``` +The output gets written to `B-ASIC/build/lib`. + +### Using setuptools to create a package +How to create a package using setuptools. + +#### Setup (Binary distribution) +In `B-ASIC`: +``` +python3 setup.py bdist_wheel +``` +The output gets written to `B-ASIC/dist`. + +#### Setup (Source distribution) +In `B-ASIC`: +``` +python3 setup.py sdist +``` +The output gets written to `B-ASIC/dist`. + +## Usage +How to build and use the library as a user. + +### Installation +``` +python3 -m pip install b_asic +``` + +### Importing +``` +python3 +>>> import b_asic as asic +>>> help(asic) +``` + +## License +B-ASIC is distributed under the MIT license. +See the included LICENSE file for more information. \ No newline at end of file diff --git a/b_asic/__init__.py b/b_asic/__init__.py new file mode 100755 index 00000000..598e2cbf --- /dev/null +++ b/b_asic/__init__.py @@ -0,0 +1,6 @@ +"""Better ASIC Toolbox""" +from _b_asic import * + +def mul(a, b): + """A function that multiplies two numbers""" + return a * b diff --git a/build.sh b/build.sh deleted file mode 100644 index 1015ced2..00000000 --- a/build.sh +++ /dev/null @@ -1,5 +0,0 @@ -#! bin/sh -for n in `find . -name "*.py"` -do - python3 $n -done \ No newline at end of file diff --git a/helloworld.py b/helloworld.py deleted file mode 100644 index 6d95fe97..00000000 --- a/helloworld.py +++ /dev/null @@ -1 +0,0 @@ -print("Hello world") \ No newline at end of file diff --git a/logo.png b/logo.png new file mode 100755 index 0000000000000000000000000000000000000000..b1bd78fe7eb8a737ac8153576078d61492c2e9a3 GIT binary patch literal 70697 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	UE(^A93S)> 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 literal 0 HcmV?d00001 diff --git a/setup.py b/setup.py new file mode 100755 index 00000000..aa6d7a6c --- /dev/null +++ b/setup.py @@ -0,0 +1,75 @@ +import os +import sys +import shutil +import subprocess +import setuptools +from setuptools import Extension +from setuptools.command.build_ext import build_ext + +CMAKE_EXE = os.environ.get('CMAKE_EXE', shutil.which('cmake')) + +class CMakeExtension(Extension): + 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.") + + 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_configure_argv = [ + CMAKE_EXE, ext.sourcedir, + "-DCMAKE_BUILD_TYPE=" + cmake_build_type, + "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=" + cmake_output_dir, + "-DPYTHON_EXECUTABLE=" + sys.executable, + ] + cmake_build_argv = [ + CMAKE_EXE, "--build", ".", + "--config", cmake_build_type + ] + + 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) + + print(f"=== Building {ext.name} ===") + print(f"Temp dir: {self.build_temp}") + print(f"Output dir: {cmake_output_dir}") + print(f"Build type: {cmake_build_type}") + subprocess.check_call(cmake_build_argv, cwd=self.build_temp, env=env) + + 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", + classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + python_requires = ">=3.6", + install_requires = ["pybind11>=2.3.0"], + packages = ["b_asic"], + ext_modules = [CMakeExtension("b_asic")], + cmdclass = {"build_ext": CMakeBuild}, + zip_safe = False +) \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100755 index 00000000..c3184d1b --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,21 @@ +#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")); +} \ No newline at end of file diff --git a/testbuild.py b/testbuild.py deleted file mode 100644 index 48f3864f..00000000 --- a/testbuild.py +++ /dev/null @@ -1 +0,0 @@ -print("Worked to build this file") \ No newline at end of file -- GitLab From 0e74e6a625e15d652d48f512323053f9306cbaaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20H=C3=A4rnqvist?= <ivarhar@outlook.com> Date: Thu, 20 Feb 2020 20:55:45 +0100 Subject: [PATCH 03/50] fix CMake python file copy destination --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b6dafb3..7f6908ed 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,8 +67,8 @@ target_link_libraries( add_custom_target( copy_python_files ALL - COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_LIST_DIR}/${LIBRARY_NAME}" "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}" - COMMENT "Copying python files to ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}" + 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}" ) add_custom_target( copy_misc_files ALL -- GitLab From dce323929a2c467ec1ab6383d6850dd2410d1604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20H=C3=A4rnqvist?= <ivarhar@outlook.com> Date: Fri, 21 Feb 2020 00:45:57 +0100 Subject: [PATCH 04/50] add python system shell --- b_asic/__init__.py | 17 ++-- b_asic/operation.py | 210 +++++++++++++++++++++++++++++++++++++++++++ b_asic/ops.py | 75 ++++++++++++++++ b_asic/pc.py | 24 +++++ b_asic/port.py | 147 ++++++++++++++++++++++++++++++ b_asic/schema.py | 24 +++++ b_asic/sfg.py | 37 ++++++++ b_asic/signal.py | 68 ++++++++++++++ b_asic/simulation.py | 42 +++++++++ src/main.cpp | 6 +- 10 files changed, 642 insertions(+), 8 deletions(-) create mode 100755 b_asic/operation.py create mode 100755 b_asic/ops.py create mode 100755 b_asic/pc.py create mode 100755 b_asic/port.py create mode 100755 b_asic/schema.py create mode 100755 b_asic/sfg.py create mode 100755 b_asic/signal.py create mode 100755 b_asic/simulation.py diff --git a/b_asic/__init__.py b/b_asic/__init__.py index 598e2cbf..8bbc17ab 100755 --- a/b_asic/__init__.py +++ b/b_asic/__init__.py @@ -1,6 +1,13 @@ -"""Better ASIC Toolbox""" +""" +Better ASIC Toolbox. +TODO: More info. +""" from _b_asic import * - -def mul(a, b): - """A function that multiplies two numbers""" - return a * b +from b_asic.operation import * +from b_asic.ops import * +from b_asic.pc import * +from b_asic.port import * +from b_asic.schema import * +from b_asic.sfg import * +from b_asic.signal import * +from b_asic.simulation import * \ No newline at end of file diff --git a/b_asic/operation.py b/b_asic/operation.py new file mode 100755 index 00000000..4bea5047 --- /dev/null +++ b/b_asic/operation.py @@ -0,0 +1,210 @@ +""" +B-ASIC Operation Module. +TODO: More info. +""" +from b_asic.port import InputPort, OutputPort +from b_asic.signal import SignalSource, SignalDestination +from b_asic.simulation import SimulationState, OperationState +from abc import ABC, abstractmethod +from numbers import Number +from typing import NewType, List, Dict, Optional, final + +OperationId = NewType("OperationId", int) + +class Operation(ABC): + """ + Operation interface. + TODO: More info. + """ + + @abstractmethod + def identifier(self) -> OperationId: + """ + Get the unique identifier. + """ + pass + + @abstractmethod + def inputs(self) -> List[InputPort]: + """ + Get a list of all input ports. + """ + pass + + @abstractmethod + def outputs(self) -> List[OutputPort]: + """ + Get a list of all output ports. + """ + pass + + @abstractmethod + def input_count(self) -> int: + """ + Get the number of input ports. + """ + pass + + @abstractmethod + def output_count(self) -> int: + """ + Get the number of output ports. + """ + pass + + @abstractmethod + def input(self, i: int) -> InputPort: + """ + Get the input port at index i. + """ + pass + + @abstractmethod + def output(self, i: int) -> OutputPort: + """ + Get the output port at index i. + """ + pass + + @abstractmethod + def params(self) -> Dict[str, Optional[Any]]: + """ + Get a dictionary of all parameter values. + """ + pass + + @abstractmethod + def param(self, name: str) -> Optional[Any]: + """ + Get the value of a parameter. + Returns None if the parameter is not defined. + """ + pass + + @abstractmethod + def set_param(self, name: str, value: Any) -> None: + """ + Set the value of a parameter. + The parameter must be defined. + """ + pass + + @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. + """ + pass + + @abstractmethod + def split(self) -> List[Operation]: + """ + Split the operation into multiple operations. + If splitting is not possible, this may return a list containing only the operation itself. + """ + pass + + # TODO: More stuff. + +class BasicOperation(ABC, Operation): + """ + Generic abstract operation class which most implementations will derive from. + TODO: More info. + """ + + _identifier: OperationId + _input_ports: List[InputPort] + _output_ports: List[OutputPort] + _parameters: Dict[str, Optional[Any]] + + def __init__(self, identifier: OperationId): + """ + Construct a BasicOperation. + """ + self._identifier = identifier + self._input_ports = [] + self._output_ports = [] + self._parameters = {} + + @abstractmethod + def evaluate(self, inputs: list) -> list: + """ + Evaluate the operation and generate a list of output values given a list of input values. + """ + pass + + @final + def id(self) -> OperationId: + return self._identifier + + @final + def inputs(self) -> List[InputPort]: + return self._input_ports.copy() + + @final + def outputs(self) -> List[OutputPort]: + return self._output_ports.copy() + + @final + def input_count(self) -> int: + return len(self._input_ports) + + @final + def output_count(self) -> int: + return len(self._output_ports) + + @final + def input(self, i: int) -> InputPort: + return self._input_ports[i] + + @final + def output(self, i: int) -> OutputPort: + return self._output_ports[i] + + @final + def params(self) -> Dict[str, Optional[Any]]: + return self._parameters.copy() + + @final + def param(self, name: str) -> Optional[Any]: + return self._parameters.get(name) + + @final + 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.identifier()] + + while self_state.iteration < state.iteration: + input_values: List[Number] = [0] * input_count + for i in range(input_count): + source: SignalSource = self._input_ports[i].signal().source + input_values[i] = source.operation.evaluate_outputs(state)[source.port_index] + + self_state.output_values = self.evaluate(input_values) + assert len(self_state.output_values) == output_count # TODO: Error message. + self_state.iteration += 1 + for i in range(output_count): + for signal in self._output_ports[i].signals(): + destination: SignalDestination = 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] + + # TODO: More stuff. \ No newline at end of file diff --git a/b_asic/ops.py b/b_asic/ops.py new file mode 100755 index 00000000..6b370725 --- /dev/null +++ b/b_asic/ops.py @@ -0,0 +1,75 @@ +""" +B-ASIC Core Operations Module. +TODO: More info. +""" + +from b_asic.operation import OperationId, Operation, BasicOperation +from numbers import Number +from typing import final + +class Input(Operation): + """ + Input operation. + TODO: More info. + """ + + # TODO: Implement. + pass + +class Constant(BasicOperation): + """ + Constant value operation. + TODO: More info. + """ + + def __init__(self, identifier: OperationId, value: Number): + """ + Construct a Constant. + """ + super().__init__(identifier) + self._output_ports = [OutputPort()] # TODO: Generate appropriate ID for ports. + self._parameters["value"] = value + + @final + def evaluate(self, inputs: list) -> list: + return [self.param("value")] + +class Addition(BasicOperation): + """ + Binary addition operation. + TODO: More info. + """ + + def __init__(self, identifier: OperationId): + """ + Construct an Addition. + """ + super().__init__(identifier) + self._input_ports = [InputPort(), InputPort()] # TODO: Generate appropriate ID for ports. + self._output_ports = [OutputPort()] # TODO: Generate appropriate ID for ports. + + @final + def evaluate(self, inputs: list) -> list: + return [inputs[0] + inputs[1]] + + +class ConstantMultiplication(BasicOperation): + """ + Unary constant multiplication operation. + TODO: More info. + """ + + def __init__(self, identifier: OperationId, coefficient: Number): + """ + Construct a ConstantMultiplication. + """ + super().__init__(identifier) + self._input_ports = [InputPort()] # TODO: Generate appropriate ID for ports. + self._output_ports = [OutputPort()] # TODO: Generate appropriate ID for ports. + self._parameters["coefficient"] = coefficient + + @final + def evaluate(self, inputs: list) -> list: + return [inputs[0] * self.param("coefficient")] + +# TODO: More operations. \ No newline at end of file diff --git a/b_asic/pc.py b/b_asic/pc.py new file mode 100755 index 00000000..cc18d6af --- /dev/null +++ b/b_asic/pc.py @@ -0,0 +1,24 @@ +""" +B-ASIC Precedence Chart Module. +TODO: More info. +""" + +from b_asic.sfg 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): + """ + Construct a PrecedenceChart. + """ + self.sfg = sfg + # TODO: Implement. + + # TODO: More stuff. \ No newline at end of file diff --git a/b_asic/port.py b/b_asic/port.py new file mode 100755 index 00000000..6b1e8d20 --- /dev/null +++ b/b_asic/port.py @@ -0,0 +1,147 @@ +""" +B-ASIC Port Module. +TODO: More info. +""" + +from b_asic.signal import Signal +from abc import ABC, abstractmethod +from typing import NewType, Optional, List, Dict, final + +PortId = NewType("PortId", int) + +class Port(ABC): + """ + Abstract port class. + TODO: More info. + """ + + _identifier: PortId + + def __init__(self, identifier: PortId): + """ + Construct a Port. + """ + self._identifier = identifier + + @final + def identifier(self) -> PortId: + """ + Get the unique identifier. + """ + return self._identifier + + @abstractmethod + def signals(self) -> List[Signal]: + """ + Get a list of all connected signals. + """ + pass + + @abstractmethod + def signal_count(self) -> int: + """ + Get the number of connected signals. + """ + pass + + @abstractmethod + def signal(self, i: int = 0) -> Signal: + """ + Get the connected signal at index i. + """ + pass + + @abstractmethod + def connect(self, signal: Signal) -> None: + """ + Connect a signal. + """ + pass + + @abstractmethod + def disconnect(self, i: int = 0) -> None: + """ + Disconnect a signal. + """ + pass + + # TODO: More stuff. + +class InputPort(Port): + """ + Input port. + TODO: More info. + """ + _source_signal: Optional[Signal] + + def __init__(self, identifier: PortId): + """ + Construct an InputPort. + """ + super().__init__(identifier) + self._source_signal = None + + @final + def signals(self) -> List[Signal]: + return [] if self._source_signal == None else [self._source_signal] + + @final + def signal_count(self) -> int: + return 0 if self._source_signal == None else 1 + + @final + def signal(self, i: int = 0) -> Signal: + assert i >= 0 and i < self.signal_count() # TODO: Error message. + assert self._source_signal != None # TODO: Error message. + return self._source_signal + + @final + def connect(self, signal: Signal) -> None: + self._source_signal = signal + + @final + def disconnect(self, i: int = 0) -> None: + assert i >= 0 and i < self.signal_count() # TODO: Error message. + self._source_signal = None + + # TODO: More stuff. + +class OutputPort(Port): + """ + Output port. + TODO: More info. + """ + + _destination_signals: List[Signal] + + def __init__(self, identifier: PortId): + """ + Construct an OutputPort. + """ + super().__init__(identifier) + self._destination_signals = [] + + @final + def signals(self) -> List[Signal]: + return self._destination_signals.copy() + + @final + def signal_count(self) -> int: + return len(self._destination_signals) + + @final + def signal(self, i: int = 0) -> Signal: + assert i >= 0 and i < self.signal_count() # TODO: Error message. + return self._destination_signals[i] + + @final + def connect(self, signal: Signal) -> None: + assert signal not in self._destination_signals # TODO: Error message. + self._destination_signals.append(signal) + + @final + def disconnect(self, i: int = 0) -> None: + assert i >= 0 and i < self.signal_count() # TODO: Error message. + del self._destination_signals[i] + + # TODO: More stuff. \ No newline at end of file diff --git a/b_asic/schema.py b/b_asic/schema.py new file mode 100755 index 00000000..a7642f49 --- /dev/null +++ b/b_asic/schema.py @@ -0,0 +1,24 @@ +""" +B-ASIC Schema Module. +TODO: More info. +""" + +from b_asic.pc import PrecedenceChart + +class Schema: + """ + Schema constructed from a precedence chart. + TODO: More info. + """ + + pc: PrecedenceChart + # TODO: More members. + + def __init__(self, pc: PrecedenceChart): + """ + Construct a Schema. + """ + self.pc = pc + # TODO: Implement. + + # TODO: More stuff. \ No newline at end of file diff --git a/b_asic/sfg.py b/b_asic/sfg.py new file mode 100755 index 00000000..d39cc524 --- /dev/null +++ b/b_asic/sfg.py @@ -0,0 +1,37 @@ +""" +B-ASIC Signal Flow Graph Module. +TODO: More info. +""" + +from b_asic.operation import OperationId, Operation, BasicOperation +from b_asic.signal import SignalSource, SignalDestination +from b_asic.simulation import SimulationState, OperationState +from typing import List, final + +class SFG(BasicOperation): + """ + Signal flow graph. + TODO: More info. + """ + + _operations: List[Operation] + + def __init__(self, identifier: OperationId, input_destinations: List[SignalDestination], output_sources: List[SignalSource]): + """ + Construct a SFG. + """ + super().__init__(identifier) + # TODO: Allocate input/output ports with appropriate IDs. + self._operations = [] + # TODO: Traverse the graph between the inputs/outputs and add to self._operations. + # TODO: Connect ports with signals with appropriate IDs. + + @final + def evaluate(self, inputs: list) -> list: + return [] # TODO: Implement + + @final + def split(self) -> List[Operation]: + return self._operations + + # TODO: More stuff. \ No newline at end of file diff --git a/b_asic/signal.py b/b_asic/signal.py new file mode 100755 index 00000000..36be9f58 --- /dev/null +++ b/b_asic/signal.py @@ -0,0 +1,68 @@ +""" +B-ASIC Signal Module. +TODO: More info. +""" + +from b_asic.operation import Operation +from typing import NewType + +SignalId = NewType("SignalId", int) + +class SignalSource: + """ + Handle to a signal source. + TODO: More info. + """ + operation: Operation + port_index: int + + def __init__(self, operation: Operation, port_index: int): + """ + Construct a SignalSource. + """ + self.operation = operation + self.port_index = port_index + + # TODO: More stuff. + +class SignalDestination: + """ + Handle to a signal destination. + TODO: More info. + """ + operation: Operation + port_index: int + + def __init__(self, operation: Operation, port_index: int): + """ + Construct a SignalDestination. + """ + self.operation = operation + self.port_index = port_index + + # TODO: More stuff. + +class Signal: + """ + A connection between two operations consisting of a source and destination handle. + TODO: More info. + """ + _identifier: SignalId + source: SignalSource + destination: SignalDestination + + def __init__(self, identifier: SignalId, source: SignalSource, destination: SignalDestination): + """ + Construct a Signal. + """ + self._identifier = identifier + self.source = source + self.destination = destination + + def identifier(self) -> SignalId: + """ + Get the unique identifier. + """ + return self._identifier + + # TODO: More stuff. \ No newline at end of file diff --git a/b_asic/simulation.py b/b_asic/simulation.py new file mode 100755 index 00000000..aa33cb33 --- /dev/null +++ b/b_asic/simulation.py @@ -0,0 +1,42 @@ +""" +B-ASIC Simulation Module. +TODO: More info. +""" + +from b_asic.operation import OperationId +from numbers import Number +from typing import List, Dict + +class OperationState: + """ + Simulation state of an operation. + TODO: More info. + """ + + output_values: List[Number] + iteration: int + + def __init__(self): + """ + Construct an OperationState. + """ + self.output_values = [] + self.iteration = 0 + +class SimulationState: + """ + Simulation state. + TODO: More info. + """ + + operation_states: Dict[OperationId, OperationState] + iteration: int + + def __init__(self): + """ + Construct a SimulationState. + """ + self.operation_states = {} + self.iteration = 0 + + # TODO: More stuff. \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index c3184d1b..75a77ef5 100755 --- a/src/main.cpp +++ b/src/main.cpp @@ -15,7 +15,7 @@ int sub(int a, int 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")); + 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")); } \ No newline at end of file -- GitLab From 8509024bd55d3b07183f7ba93d56a0112b7b3dfe Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Fri, 21 Feb 2020 08:22:25 +0100 Subject: [PATCH 05/50] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f126999b..20c8c4a2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ <img src="https://files.slack.com/files-pri/TSHPRJY83-FTTRW9MQ8/b-asic-logo-opaque.png" width="318" height="100"> <br> -<h3>The leading company in circuit design<h3> \ No newline at end of file +<h3>B-ASIC is an ASIC toolbox for Python 3 that simplifies circuit design and optimization.<h3> \ No newline at end of file -- GitLab From acc9f36e4d6f5def4d3b2e4c382023f811ac5e8f Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Fri, 21 Feb 2020 09:05:25 +0100 Subject: [PATCH 06/50] Delete helloworld.py --- helloworld.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 helloworld.py diff --git a/helloworld.py b/helloworld.py deleted file mode 100644 index 6d95fe97..00000000 --- a/helloworld.py +++ /dev/null @@ -1 +0,0 @@ -print("Hello world") \ No newline at end of file -- GitLab From b164384a32d0e3ee35b2b192651720581790b2d4 Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Fri, 21 Feb 2020 09:07:06 +0100 Subject: [PATCH 07/50] Update .gitlab-ci.yml --- .gitlab-ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2130f26d..78499e52 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,5 +6,4 @@ PythonBuild: artifacts: untracked: true script: - - apt-get update && apt-get install python3 -y - - python3 helloworld.py \ No newline at end of file + - apt-get update && apt-get install python3 -y \ No newline at end of file -- GitLab From 613e90981a08500a1215f80a98ae7b8dcf34d7b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20H=C3=A4rnqvist?= <ivaha717@student.liu.se> Date: Mon, 24 Feb 2020 10:20:44 +0100 Subject: [PATCH 08/50] fix sdist by adding CMakeLists.txt to manifest, update README.md --- .gitignore | 0 CMakeLists.txt | 0 LICENSE | 0 MANIFEST.in | 1 + README.md | 12 ++++++++++++ b_asic/__init__.py | 0 b_asic/operation.py | 0 b_asic/ops.py | 0 b_asic/pc.py | 0 b_asic/port.py | 0 b_asic/schema.py | 0 b_asic/sfg.py | 0 b_asic/signal.py | 0 b_asic/simulation.py | 0 logo.png | Bin setup.py | 0 src/main.cpp | 0 17 files changed, 13 insertions(+) mode change 100755 => 100644 .gitignore mode change 100755 => 100644 CMakeLists.txt mode change 100755 => 100644 LICENSE mode change 100755 => 100644 MANIFEST.in mode change 100755 => 100644 README.md mode change 100755 => 100644 b_asic/__init__.py mode change 100755 => 100644 b_asic/operation.py mode change 100755 => 100644 b_asic/ops.py mode change 100755 => 100644 b_asic/pc.py mode change 100755 => 100644 b_asic/port.py mode change 100755 => 100644 b_asic/schema.py mode change 100755 => 100644 b_asic/sfg.py mode change 100755 => 100644 b_asic/signal.py mode change 100755 => 100644 b_asic/simulation.py mode change 100755 => 100644 logo.png mode change 100755 => 100644 setup.py mode change 100755 => 100644 src/main.cpp diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 diff --git a/CMakeLists.txt b/CMakeLists.txt old mode 100755 new mode 100644 diff --git a/LICENSE b/LICENSE old mode 100755 new mode 100644 diff --git a/MANIFEST.in b/MANIFEST.in old mode 100755 new mode 100644 index 89a500ee..ce996f6c --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ include README.md include LICENSE +include CMakeLists.txt recursive-include src *.cpp *.h diff --git a/README.md b/README.md old mode 100755 new mode 100644 index 78df523e..4917b55e --- a/README.md +++ b/README.md @@ -59,6 +59,18 @@ python3 setup.py sdist ``` The output gets written to `B-ASIC/dist`. +#### Installation (Binary distribution) +In `B-ASIC`: +``` +python3 -m pip install b_asic-<version>-<cpver>-<cpver>_<arch>.whl +``` + +#### Installation (Source distribution) +In `B-ASIC`: +``` +python3 -m pip install b-asic-<version>.tar.gz +``` + ## Usage How to build and use the library as a user. diff --git a/b_asic/__init__.py b/b_asic/__init__.py old mode 100755 new mode 100644 diff --git a/b_asic/operation.py b/b_asic/operation.py old mode 100755 new mode 100644 diff --git a/b_asic/ops.py b/b_asic/ops.py old mode 100755 new mode 100644 diff --git a/b_asic/pc.py b/b_asic/pc.py old mode 100755 new mode 100644 diff --git a/b_asic/port.py b/b_asic/port.py old mode 100755 new mode 100644 diff --git a/b_asic/schema.py b/b_asic/schema.py old mode 100755 new mode 100644 diff --git a/b_asic/sfg.py b/b_asic/sfg.py old mode 100755 new mode 100644 diff --git a/b_asic/signal.py b/b_asic/signal.py old mode 100755 new mode 100644 diff --git a/b_asic/simulation.py b/b_asic/simulation.py old mode 100755 new mode 100644 diff --git a/logo.png b/logo.png old mode 100755 new mode 100644 diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 diff --git a/src/main.cpp b/src/main.cpp old mode 100755 new mode 100644 -- GitLab From 21124444435a48f821538c4747afb8e92c6536a1 Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Mon, 24 Feb 2020 10:55:55 +0100 Subject: [PATCH 09/50] Deleted README.md --- README.md | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 README.md diff --git a/README.md b/README.md deleted file mode 100644 index 20c8c4a2..00000000 --- a/README.md +++ /dev/null @@ -1,3 +0,0 @@ -<img src="https://files.slack.com/files-pri/TSHPRJY83-FTTRW9MQ8/b-asic-logo-opaque.png" width="318" height="100"> -<br> -<h3>B-ASIC is an ASIC toolbox for Python 3 that simplifies circuit design and optimization.<h3> \ No newline at end of file -- GitLab From f1b026aa955bb5baed73467159fa8bafb2b751ad Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Mon, 24 Feb 2020 11:00:59 +0100 Subject: [PATCH 10/50] Revert "Deleted README.md" This reverts commit 21124444435a48f821538c4747afb8e92c6536a1 --- README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..20c8c4a2 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +<img src="https://files.slack.com/files-pri/TSHPRJY83-FTTRW9MQ8/b-asic-logo-opaque.png" width="318" height="100"> +<br> +<h3>B-ASIC is an ASIC toolbox for Python 3 that simplifies circuit design and optimization.<h3> \ No newline at end of file -- GitLab From 66a79f9c4a20a5c50d461a8ca9c58f56149f245b Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Mon, 24 Feb 2020 11:15:53 +0100 Subject: [PATCH 11/50] Readded yaml file --- .gitlab-ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..78499e52 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,9 @@ +stages: + - build + +PythonBuild: + stage: build + artifacts: + untracked: true + script: + - apt-get update && apt-get install python3 -y \ No newline at end of file -- GitLab From 415be01e3c073e4081ff1f8dec338796fae6d896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20H=C3=A4rnqvist?= <ivaha717@student.liu.se> Date: Tue, 25 Feb 2020 00:04:55 +0100 Subject: [PATCH 12/50] fix cmake build output directory on windows --- .gitignore | 3 ++- CMakeLists.txt | 35 ++++++++++++++++++----------------- LICENSE | 2 +- README.md | 8 ++++---- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 473e0e34..36987299 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,5 @@ ehthumbs_vista.db $RECYCLE.BIN/ *.stackdump [Dd]esktop.ini -*.egg-info \ No newline at end of file +*.egg-info +__pycache__/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f6908ed..c2381e4d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,24 +12,26 @@ find_package(pybind11 CONFIG REQUIRED) set(LIBRARY_NAME "b_asic") set(TARGET_NAME "_${LIBRARY_NAME}") + if(NOT CMAKE_LIBRARY_OUTPUT_DIRECTORY) include(GNUInstallDirs) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_INSTALL_LIBDIR}") endif() +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_DEBUG "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") +set(CMAKE_PDB_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") +set(CMAKE_PDB_OUTPUT_DIRECTORY_DEBUG "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") +set(CMAKE_PDB_OUTPUT_DIRECTORY_RELEASE "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") +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_library( - "${TARGET_NAME}" MODULE - "${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp" -) -add_library( - "${TARGET_NAME}:${TARGET_NAME}" - ALIAS "${TARGET_NAME}" -) - -set_target_properties( +pybind11_add_module( "${TARGET_NAME}" - PROPERTIES - PREFIX "" + "${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp" ) target_include_directories( @@ -47,14 +49,14 @@ target_compile_options( "${TARGET_NAME}" PRIVATE $<$<OR:$<CXX_COMPILER_ID:GNU>,$<CXX_COMPILER_ID:Clang>>: - -W -Wall -Wextra -Werror -Wno-psabi -fvisibility=hidden + -W -Wall -Wextra -Werror -Wno-psabi $<$<CONFIG:Debug>:-g> - $<$<NOT:$<CONFIG:Debug>>:-O3> + $<$<NOT:$<CONFIG:Debug>>:-O3 -flto> > $<$<CXX_COMPILER_ID:MSVC>: - /W3 /WX /permissive- /utf-8 + /W3 /WX /permissive- /utf-8 /bigobj $<$<CONFIG:Debug>:/Od> - $<$<NOT:$<CONFIG:Debug>>:/Ot> + $<$<NOT:$<CONFIG:Debug>>:/Ot /GL /LTCG> > ) @@ -62,7 +64,6 @@ target_link_libraries( "${TARGET_NAME}" PRIVATE fmt::fmt-header-only - pybind11::module ) add_custom_target( diff --git a/LICENSE b/LICENSE index 17010bd5..669ce41e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 TDDD96 +Copyright (c) 2020 TDDD96 PUM4 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4917b55e..b36a5d82 100644 --- a/README.md +++ b/README.md @@ -43,26 +43,26 @@ cmake --build . --config Release The output gets written to `B-ASIC/build/lib`. ### Using setuptools to create a package -How to create a package using setuptools. +How to create a package using setuptools that can be installed using pip. #### Setup (Binary distribution) In `B-ASIC`: ``` python3 setup.py bdist_wheel ``` -The output gets written to `B-ASIC/dist`. +The output gets written to `B-ASIC/dist/b_asic-<version>-<python_tag>-<abi_tag>-<platform_tag>.whl`. #### Setup (Source distribution) In `B-ASIC`: ``` python3 setup.py sdist ``` -The output gets written to `B-ASIC/dist`. +The output gets written to `B-ASIC/dist/b-asic-<version>.tar.gz`. #### Installation (Binary distribution) In `B-ASIC`: ``` -python3 -m pip install b_asic-<version>-<cpver>-<cpver>_<arch>.whl +python3 -m pip install b_asic-<version>-<python_tag>-<abi_tag>-<platform_tag>.whl ``` #### Installation (Source distribution) -- GitLab From 46082d46e89aa63349e02dd9ea5106d592a1aedb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20H=C3=A4rnqvist?= <ivaha717@student.liu.se> Date: Tue, 25 Feb 2020 00:06:41 +0100 Subject: [PATCH 13/50] remove bad compiler flags --- CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c2381e4d..8ba064fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,12 +51,12 @@ target_compile_options( $<$<OR:$<CXX_COMPILER_ID:GNU>,$<CXX_COMPILER_ID:Clang>>: -W -Wall -Wextra -Werror -Wno-psabi $<$<CONFIG:Debug>:-g> - $<$<NOT:$<CONFIG:Debug>>:-O3 -flto> + $<$<NOT:$<CONFIG:Debug>>:-O3> > $<$<CXX_COMPILER_ID:MSVC>: - /W3 /WX /permissive- /utf-8 /bigobj + /W3 /WX /permissive- /utf-8 $<$<CONFIG:Debug>:/Od> - $<$<NOT:$<CONFIG:Debug>>:/Ot /GL /LTCG> + $<$<NOT:$<CONFIG:Debug>>:/Ot> > ) -- GitLab From a5069646bea7bb132ada81ddec26334be6410915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20H=C3=A4rnqvist?= <ivaha717@student.liu.se> Date: Tue, 25 Feb 2020 01:05:00 +0100 Subject: [PATCH 14/50] fix circular module import --- CMakeLists.txt | 8 +- README.md | 2 + b_asic/__init__.py | 7 +- b_asic/basic_operation.py | 114 ++++++++++++++++++++++ b_asic/{ops.py => core_operations.py} | 4 +- b_asic/operation.py | 124 +++--------------------- b_asic/{pc.py => precedence_chart.py} | 2 +- b_asic/schema.py | 2 +- b_asic/{sfg.py => signal_flow_graph.py} | 3 +- setup.py | 6 +- 10 files changed, 152 insertions(+), 120 deletions(-) create mode 100644 b_asic/basic_operation.py rename b_asic/{ops.py => core_operations.py} (91%) rename b_asic/{pc.py => precedence_chart.py} (89%) rename b_asic/{sfg.py => signal_flow_graph.py} (89%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ba064fd..433d2746 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,9 +67,15 @@ target_link_libraries( ) add_custom_target( - copy_python_files ALL + 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 remove_old_python_dir ) add_custom_target( copy_misc_files ALL diff --git a/README.md b/README.md index b36a5d82..bcd09857 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ The following packages are required in order to build the library: * setuptools * wheel * pybind11 + * numpy + * pyside2/pyqt5 ## Development How to build and debug the library during development. diff --git a/b_asic/__init__.py b/b_asic/__init__.py index 8bbc17ab..8a84a945 100644 --- a/b_asic/__init__.py +++ b/b_asic/__init__.py @@ -3,11 +3,12 @@ Better ASIC Toolbox. TODO: More info. """ from _b_asic import * +from b_asic.basic_operation import * +from b_asic.core_operations import * from b_asic.operation import * -from b_asic.ops import * -from b_asic.pc import * +from b_asic.precedence_chart import * from b_asic.port import * from b_asic.schema import * -from b_asic.sfg import * +from b_asic.signal_flow_graph import * from b_asic.signal import * from b_asic.simulation import * \ No newline at end of file diff --git a/b_asic/basic_operation.py b/b_asic/basic_operation.py new file mode 100644 index 00000000..e87860c6 --- /dev/null +++ b/b_asic/basic_operation.py @@ -0,0 +1,114 @@ +""" +B-ASIC Basic Operation Module. +TODO: More info. +""" + +from b_asic.port import InputPort, OutputPort +from b_asic.signal import SignalSource, SignalDestination +from b_asic.operation import OperationId, Operation +from b_asic.simulation import SimulationState, OperationState +from abc import ABC, abstractmethod +from typing import List, Dict, Optional, Any, final +from numbers import Number + +class BasicOperation(Operation): + """ + Generic abstract operation class which most implementations will derive from. + TODO: More info. + """ + + _identifier: OperationId + _input_ports: List[InputPort] + _output_ports: List[OutputPort] + _parameters: Dict[str, Optional[Any]] + + def __init__(self, identifier: OperationId): + """ + Construct a BasicOperation. + """ + self._identifier = identifier + self._input_ports = [] + self._output_ports = [] + self._parameters = {} + + @abstractmethod + def evaluate(self, inputs: list) -> list: + """ + Evaluate the operation and generate a list of output values given a list of input values. + """ + pass + + @final + def id(self) -> OperationId: + return self._identifier + + @final + def inputs(self) -> List[InputPort]: + return self._input_ports.copy() + + @final + def outputs(self) -> List[OutputPort]: + return self._output_ports.copy() + + @final + def input_count(self) -> int: + return len(self._input_ports) + + @final + def output_count(self) -> int: + return len(self._output_ports) + + @final + def input(self, i: int) -> InputPort: + return self._input_ports[i] + + @final + def output(self, i: int) -> OutputPort: + return self._output_ports[i] + + @final + def params(self) -> Dict[str, Optional[Any]]: + return self._parameters.copy() + + @final + def param(self, name: str) -> Optional[Any]: + return self._parameters.get(name) + + @final + 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.identifier()] + + while self_state.iteration < state.iteration: + input_values: List[Number] = [0] * input_count + for i in range(input_count): + source: SignalSource = self._input_ports[i].signal().source + input_values[i] = source.operation.evaluate_outputs(state)[source.port_index] + + self_state.output_values = self.evaluate(input_values) + assert len(self_state.output_values) == output_count # TODO: Error message. + self_state.iteration += 1 + for i in range(output_count): + for signal in self._output_ports[i].signals(): + destination: SignalDestination = 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] + + # TODO: More stuff. \ No newline at end of file diff --git a/b_asic/ops.py b/b_asic/core_operations.py similarity index 91% rename from b_asic/ops.py rename to b_asic/core_operations.py index 6b370725..553d7cff 100644 --- a/b_asic/ops.py +++ b/b_asic/core_operations.py @@ -3,7 +3,9 @@ B-ASIC Core Operations Module. TODO: More info. """ -from b_asic.operation import OperationId, Operation, BasicOperation +from b_asic.port import InputPort, OutputPort +from b_asic.operation import OperationId, Operation +from b_asic.basic_operation import BasicOperation from numbers import Number from typing import final diff --git a/b_asic/operation.py b/b_asic/operation.py index 4bea5047..310491bc 100644 --- a/b_asic/operation.py +++ b/b_asic/operation.py @@ -2,12 +2,14 @@ B-ASIC Operation Module. TODO: More info. """ -from b_asic.port import InputPort, OutputPort -from b_asic.signal import SignalSource, SignalDestination -from b_asic.simulation import SimulationState, OperationState + from abc import ABC, abstractmethod from numbers import Number -from typing import NewType, List, Dict, Optional, final +from typing import NewType, List, Dict, Optional, Any, TYPE_CHECKING + +if TYPE_CHECKING: + from b_asic.port import InputPort, OutputPort + from b_asic.simulation import SimulationState OperationId = NewType("OperationId", int) @@ -21,18 +23,19 @@ class Operation(ABC): def identifier(self) -> OperationId: """ Get the unique identifier. + TODO: Move id info to SFG, remove id class members. """ pass @abstractmethod - def inputs(self) -> List[InputPort]: + def inputs(self) -> "List[InputPort]": """ Get a list of all input ports. """ pass @abstractmethod - def outputs(self) -> List[OutputPort]: + def outputs(self) -> "List[OutputPort]": """ Get a list of all output ports. """ @@ -53,14 +56,14 @@ class Operation(ABC): pass @abstractmethod - def input(self, i: int) -> InputPort: + def input(self, i: int) -> "InputPort": """ Get the input port at index i. """ pass @abstractmethod - def output(self, i: int) -> OutputPort: + def output(self, i: int) -> "OutputPort": """ Get the output port at index i. """ @@ -90,7 +93,7 @@ class Operation(ABC): pass @abstractmethod - def evaluate_outputs(self, state: SimulationState) -> List[Number]: + 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. @@ -98,7 +101,7 @@ class Operation(ABC): pass @abstractmethod - def split(self) -> List[Operation]: + def split(self) -> "List[Operation]": """ Split the operation into multiple operations. If splitting is not possible, this may return a list containing only the operation itself. @@ -107,104 +110,3 @@ class Operation(ABC): # TODO: More stuff. -class BasicOperation(ABC, Operation): - """ - Generic abstract operation class which most implementations will derive from. - TODO: More info. - """ - - _identifier: OperationId - _input_ports: List[InputPort] - _output_ports: List[OutputPort] - _parameters: Dict[str, Optional[Any]] - - def __init__(self, identifier: OperationId): - """ - Construct a BasicOperation. - """ - self._identifier = identifier - self._input_ports = [] - self._output_ports = [] - self._parameters = {} - - @abstractmethod - def evaluate(self, inputs: list) -> list: - """ - Evaluate the operation and generate a list of output values given a list of input values. - """ - pass - - @final - def id(self) -> OperationId: - return self._identifier - - @final - def inputs(self) -> List[InputPort]: - return self._input_ports.copy() - - @final - def outputs(self) -> List[OutputPort]: - return self._output_ports.copy() - - @final - def input_count(self) -> int: - return len(self._input_ports) - - @final - def output_count(self) -> int: - return len(self._output_ports) - - @final - def input(self, i: int) -> InputPort: - return self._input_ports[i] - - @final - def output(self, i: int) -> OutputPort: - return self._output_ports[i] - - @final - def params(self) -> Dict[str, Optional[Any]]: - return self._parameters.copy() - - @final - def param(self, name: str) -> Optional[Any]: - return self._parameters.get(name) - - @final - 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.identifier()] - - while self_state.iteration < state.iteration: - input_values: List[Number] = [0] * input_count - for i in range(input_count): - source: SignalSource = self._input_ports[i].signal().source - input_values[i] = source.operation.evaluate_outputs(state)[source.port_index] - - self_state.output_values = self.evaluate(input_values) - assert len(self_state.output_values) == output_count # TODO: Error message. - self_state.iteration += 1 - for i in range(output_count): - for signal in self._output_ports[i].signals(): - destination: SignalDestination = 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] - - # TODO: More stuff. \ No newline at end of file diff --git a/b_asic/pc.py b/b_asic/precedence_chart.py similarity index 89% rename from b_asic/pc.py rename to b_asic/precedence_chart.py index cc18d6af..76a22c9f 100644 --- a/b_asic/pc.py +++ b/b_asic/precedence_chart.py @@ -3,7 +3,7 @@ B-ASIC Precedence Chart Module. TODO: More info. """ -from b_asic.sfg import SFG +from b_asic.signal_flow_graph import SFG class PrecedenceChart: """ diff --git a/b_asic/schema.py b/b_asic/schema.py index a7642f49..c7a72526 100644 --- a/b_asic/schema.py +++ b/b_asic/schema.py @@ -3,7 +3,7 @@ B-ASIC Schema Module. TODO: More info. """ -from b_asic.pc import PrecedenceChart +from b_asic.precedence_chart import PrecedenceChart class Schema: """ diff --git a/b_asic/sfg.py b/b_asic/signal_flow_graph.py similarity index 89% rename from b_asic/sfg.py rename to b_asic/signal_flow_graph.py index d39cc524..8fbbebff 100644 --- a/b_asic/sfg.py +++ b/b_asic/signal_flow_graph.py @@ -3,7 +3,8 @@ B-ASIC Signal Flow Graph Module. TODO: More info. """ -from b_asic.operation import OperationId, Operation, BasicOperation +from b_asic.operation import OperationId, Operation +from b_asic.basic_operation import BasicOperation from b_asic.signal import SignalSource, SignalDestination from b_asic.simulation import SimulationState, OperationState from typing import List, final diff --git a/setup.py b/setup.py index aa6d7a6c..71ae4e4a 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,11 @@ setuptools.setup( "Operating System :: OS Independent", ], python_requires = ">=3.6", - install_requires = ["pybind11>=2.3.0"], + install_requires = [ + "pybind11>=2.3.0", + "numpy", + "install_qt_binding" + ], packages = ["b_asic"], ext_modules = [CMakeExtension("b_asic")], cmdclass = {"build_ext": CMakeBuild}, -- GitLab From 706132608ed1021c6e556b8281123a7d5fc51b1e Mon Sep 17 00:00:00 2001 From: Jacob Wahlman <jacwa448@student.liu.se> Date: Tue, 25 Feb 2020 10:30:57 +0100 Subject: [PATCH 15/50] Removed final to support < 3.8, spacing between classes --- b_asic/basic_operation.py | 21 ++++++--------------- b_asic/core_operations.py | 9 ++++----- b_asic/operation.py | 3 ++- b_asic/port.py | 28 ++++++++++------------------ b_asic/precedence_chart.py | 3 ++- b_asic/schema.py | 3 ++- b_asic/signal.py | 5 ++++- b_asic/signal_flow_graph.py | 7 +++---- b_asic/simulation.py | 4 +++- 9 files changed, 36 insertions(+), 47 deletions(-) diff --git a/b_asic/basic_operation.py b/b_asic/basic_operation.py index e87860c6..6810a9a7 100644 --- a/b_asic/basic_operation.py +++ b/b_asic/basic_operation.py @@ -8,9 +8,10 @@ from b_asic.signal import SignalSource, SignalDestination from b_asic.operation import OperationId, Operation from b_asic.simulation import SimulationState, OperationState from abc import ABC, abstractmethod -from typing import List, Dict, Optional, Any, final +from typing import List, Dict, Optional, Any from numbers import Number + class BasicOperation(Operation): """ Generic abstract operation class which most implementations will derive from. @@ -38,43 +39,33 @@ class BasicOperation(Operation): """ pass - @final def id(self) -> OperationId: return self._identifier - - @final + def inputs(self) -> List[InputPort]: return self._input_ports.copy() - @final def outputs(self) -> List[OutputPort]: return self._output_ports.copy() - @final def input_count(self) -> int: return len(self._input_ports) - @final def output_count(self) -> int: return len(self._output_ports) - @final def input(self, i: int) -> InputPort: return self._input_ports[i] - @final def output(self, i: int) -> OutputPort: return self._output_ports[i] - @final def params(self) -> Dict[str, Optional[Any]]: return self._parameters.copy() - - @final + def param(self, name: str) -> Optional[Any]: return self._parameters.get(name) - @final def set_param(self, name: str, value: Any) -> None: assert name in self._parameters # TODO: Error message. self._parameters[name] = value @@ -110,5 +101,5 @@ class BasicOperation(Operation): if all(isinstance(e, Operation) for e in results): return results return [self] - - # TODO: More stuff. \ No newline at end of file + + # TODO: More stuff. diff --git a/b_asic/core_operations.py b/b_asic/core_operations.py index 553d7cff..c766d06c 100644 --- a/b_asic/core_operations.py +++ b/b_asic/core_operations.py @@ -7,7 +7,7 @@ from b_asic.port import InputPort, OutputPort from b_asic.operation import OperationId, Operation from b_asic.basic_operation import BasicOperation from numbers import Number -from typing import final + class Input(Operation): """ @@ -18,6 +18,7 @@ class Input(Operation): # TODO: Implement. pass + class Constant(BasicOperation): """ Constant value operation. @@ -32,10 +33,10 @@ class Constant(BasicOperation): self._output_ports = [OutputPort()] # TODO: Generate appropriate ID for ports. self._parameters["value"] = value - @final def evaluate(self, inputs: list) -> list: return [self.param("value")] + class Addition(BasicOperation): """ Binary addition operation. @@ -50,7 +51,6 @@ class Addition(BasicOperation): self._input_ports = [InputPort(), InputPort()] # TODO: Generate appropriate ID for ports. self._output_ports = [OutputPort()] # TODO: Generate appropriate ID for ports. - @final def evaluate(self, inputs: list) -> list: return [inputs[0] + inputs[1]] @@ -70,8 +70,7 @@ class ConstantMultiplication(BasicOperation): self._output_ports = [OutputPort()] # TODO: Generate appropriate ID for ports. self._parameters["coefficient"] = coefficient - @final def evaluate(self, inputs: list) -> list: return [inputs[0] * self.param("coefficient")] -# TODO: More operations. \ No newline at end of file +# TODO: More operations. diff --git a/b_asic/operation.py b/b_asic/operation.py index 310491bc..731822f1 100644 --- a/b_asic/operation.py +++ b/b_asic/operation.py @@ -13,6 +13,7 @@ if TYPE_CHECKING: OperationId = NewType("OperationId", int) + class Operation(ABC): """ Operation interface. @@ -107,6 +108,6 @@ class Operation(ABC): If splitting is not possible, this may return a list containing only the operation itself. """ pass - + # TODO: More stuff. diff --git a/b_asic/port.py b/b_asic/port.py index 6b1e8d20..2d5405ed 100644 --- a/b_asic/port.py +++ b/b_asic/port.py @@ -5,10 +5,11 @@ TODO: More info. from b_asic.signal import Signal from abc import ABC, abstractmethod -from typing import NewType, Optional, List, Dict, final +from typing import NewType, Optional, List, Dict PortId = NewType("PortId", int) + class Port(ABC): """ Abstract port class. @@ -22,8 +23,7 @@ class Port(ABC): Construct a Port. """ self._identifier = identifier - - @final + def identifier(self) -> PortId: """ Get the unique identifier. @@ -36,7 +36,7 @@ class Port(ABC): Get a list of all connected signals. """ pass - + @abstractmethod def signal_count(self) -> int: """ @@ -67,6 +67,7 @@ class Port(ABC): # TODO: More stuff. + class InputPort(Port): """ Input port. @@ -81,31 +82,27 @@ class InputPort(Port): super().__init__(identifier) self._source_signal = None - @final def signals(self) -> List[Signal]: return [] if self._source_signal == None else [self._source_signal] - - @final + def signal_count(self) -> int: return 0 if self._source_signal == None else 1 - @final def signal(self, i: int = 0) -> Signal: assert i >= 0 and i < self.signal_count() # TODO: Error message. assert self._source_signal != None # TODO: Error message. return self._source_signal - @final def connect(self, signal: Signal) -> None: self._source_signal = signal - @final def disconnect(self, i: int = 0) -> None: assert i >= 0 and i < self.signal_count() # TODO: Error message. self._source_signal = None # TODO: More stuff. + class OutputPort(Port): """ Output port. @@ -121,27 +118,22 @@ class OutputPort(Port): super().__init__(identifier) self._destination_signals = [] - @final def signals(self) -> List[Signal]: return self._destination_signals.copy() - @final def signal_count(self) -> int: return len(self._destination_signals) - @final def signal(self, i: int = 0) -> Signal: assert i >= 0 and i < self.signal_count() # TODO: Error message. return self._destination_signals[i] - @final def connect(self, signal: Signal) -> None: assert signal not in self._destination_signals # TODO: Error message. self._destination_signals.append(signal) - - @final + def disconnect(self, i: int = 0) -> None: assert i >= 0 and i < self.signal_count() # TODO: Error message. del self._destination_signals[i] - - # TODO: More stuff. \ No newline at end of file + + # TODO: More stuff. diff --git a/b_asic/precedence_chart.py b/b_asic/precedence_chart.py index 76a22c9f..329c78d2 100644 --- a/b_asic/precedence_chart.py +++ b/b_asic/precedence_chart.py @@ -5,6 +5,7 @@ TODO: More info. from b_asic.signal_flow_graph import SFG + class PrecedenceChart: """ Precedence chart constructed from a signal flow graph. @@ -21,4 +22,4 @@ class PrecedenceChart: self.sfg = sfg # TODO: Implement. - # TODO: More stuff. \ No newline at end of file + # TODO: More stuff. diff --git a/b_asic/schema.py b/b_asic/schema.py index c7a72526..56eb47ff 100644 --- a/b_asic/schema.py +++ b/b_asic/schema.py @@ -5,6 +5,7 @@ TODO: More info. from b_asic.precedence_chart import PrecedenceChart + class Schema: """ Schema constructed from a precedence chart. @@ -21,4 +22,4 @@ class Schema: self.pc = pc # TODO: Implement. - # TODO: More stuff. \ No newline at end of file + # TODO: More stuff. diff --git a/b_asic/signal.py b/b_asic/signal.py index 36be9f58..4fac563f 100644 --- a/b_asic/signal.py +++ b/b_asic/signal.py @@ -8,6 +8,7 @@ from typing import NewType SignalId = NewType("SignalId", int) + class SignalSource: """ Handle to a signal source. @@ -25,6 +26,7 @@ class SignalSource: # TODO: More stuff. + class SignalDestination: """ Handle to a signal destination. @@ -42,6 +44,7 @@ class SignalDestination: # TODO: More stuff. + class Signal: """ A connection between two operations consisting of a source and destination handle. @@ -65,4 +68,4 @@ class Signal: """ return self._identifier - # TODO: More stuff. \ No newline at end of file + # TODO: More stuff. diff --git a/b_asic/signal_flow_graph.py b/b_asic/signal_flow_graph.py index 8fbbebff..f9671636 100644 --- a/b_asic/signal_flow_graph.py +++ b/b_asic/signal_flow_graph.py @@ -7,7 +7,8 @@ from b_asic.operation import OperationId, Operation from b_asic.basic_operation import BasicOperation from b_asic.signal import SignalSource, SignalDestination from b_asic.simulation import SimulationState, OperationState -from typing import List, final +from typing import List + class SFG(BasicOperation): """ @@ -27,12 +28,10 @@ class SFG(BasicOperation): # TODO: Traverse the graph between the inputs/outputs and add to self._operations. # TODO: Connect ports with signals with appropriate IDs. - @final def evaluate(self, inputs: list) -> list: return [] # TODO: Implement - @final def split(self) -> List[Operation]: return self._operations - # TODO: More stuff. \ No newline at end of file + # TODO: More stuff. diff --git a/b_asic/simulation.py b/b_asic/simulation.py index aa33cb33..e219445b 100644 --- a/b_asic/simulation.py +++ b/b_asic/simulation.py @@ -7,6 +7,7 @@ from b_asic.operation import OperationId from numbers import Number from typing import List, Dict + class OperationState: """ Simulation state of an operation. @@ -23,6 +24,7 @@ class OperationState: self.output_values = [] self.iteration = 0 + class SimulationState: """ Simulation state. @@ -39,4 +41,4 @@ class SimulationState: self.operation_states = {} self.iteration = 0 - # TODO: More stuff. \ No newline at end of file + # TODO: More stuff. -- GitLab From aacf7a8385a79d1dd494dfeba9d09d3a797c70ef Mon Sep 17 00:00:00 2001 From: Kevin Scott <kevsc634@student.liu.se> Date: Tue, 25 Feb 2020 11:54:30 +0100 Subject: [PATCH 16/50] Added support for pytest and updated readme --- README.md | 13 ++++++++++++- setup.py | 3 ++- tests/__init__.py | 0 tests/test_port.py | 10 ++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/test_port.py diff --git a/README.md b/README.md index bcd09857..97c65156 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,17 @@ python3 >>> help(asic) ``` +## Running tests +### pytest +Run all tests against installed version off b_asic using a virtual enviroment +In `B-ASIC`: +``` +python3 -m venv .venv +source .venv/bin/activate +pip install . +pytest +``` + ## License B-ASIC is distributed under the MIT license. -See the included LICENSE file for more information. \ No newline at end of file +See the included LICENSE file for more information. diff --git a/setup.py b/setup.py index 71ae4e4a..0b591cbb 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,8 @@ setuptools.setup( install_requires = [ "pybind11>=2.3.0", "numpy", - "install_qt_binding" + "install_qt_binding", + "pytest==5.3.4" ], packages = ["b_asic"], ext_modules = [CMakeExtension("b_asic")], diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_port.py b/tests/test_port.py new file mode 100644 index 00000000..498233bb --- /dev/null +++ b/tests/test_port.py @@ -0,0 +1,10 @@ +from b_asic import InputPort +import pytest + +@pytest.fixture +def outp_port(): + return InputPort(0) + + +def test_port(outp_port): + assert outp_port.signals() == [] -- GitLab From 94a50a8e9abf35c607f128c1fc124cbe279f8fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20H=C3=A4rnqvist?= <ivaha717@student.liu.se> Date: Tue, 25 Feb 2020 12:29:58 +0100 Subject: [PATCH 17/50] add clang-format, add basic ctest files, fix README --- .clang-format | 151 ++++++++++++++++++++++++++++++++++++++++++++ CMakeLists.txt | 8 ++- README.md | 4 +- test/CMakeLists.txt | 9 +++ test/test_main.cpp | 5 ++ 5 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 .clang-format create mode 100644 test/CMakeLists.txt create mode 100644 test/test_main.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..7548f76b --- /dev/null +++ b/.clang-format @@ -0,0 +1,151 @@ +AccessModifierOffset: -4 + +AlignAfterOpenBracket: DontAlign +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignConsecutiveMacros: true +AlignEscapedNewlines: DontAlign +AlignOperands: false +AlignTrailingComments: true + +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Inline +AllowShortLoopsOnASingleLine: false + +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes + +BinPackArguments: false +BinPackParameters: true + +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true + +BreakBeforeBinaryOperators: None +BreakBeforeTernaryOperators: false +BreakConstructorInitializers: BeforeComma +BreakInheritanceList: BeforeComma +BreakStringLiterals: true + +ColumnLimit: 140 + +CommentPragmas: '' + +CompactNamespaces: false + +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 + +ContinuationIndentWidth: 4 + +Cpp11BracedListStyle: true + +DerivePointerAlignment: false + +DisableFormat: false + +FixNamespaceComments: true + +ForEachMacros: + - Q_FOREACH + - BOOST_FOREACH + - FOREACH + - FOR_EACH + +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^<' + Priority: 2 + - Regex: '.*' + Priority: 1 +IncludeIsMainRegex: '(_test)?$' + +IndentCaseLabels: true +IndentGotoLabels: false +IndentPPDirectives: None +IndentWidth: 4 +IndentWrappedFunctionNames: false + +KeepEmptyLinesAtTheStartOfBlocks: false + +Language: Cpp + +MacroBlockBegin: '' +MacroBlockEnd: '' + +MaxEmptyLinesToKeep: 1 + +NamespaceIndentation: None +NamespaceMacros: + - NAMESPACE + +PenaltyBreakAssignment: 100 +PenaltyBreakBeforeFirstCallParameter: 10 +PenaltyBreakComment: 10 +PenaltyBreakFirstLessLess: 100 +PenaltyBreakString: 10 +PenaltyBreakTemplateDeclaration: 10000 +PenaltyExcessCharacter: 999999 +PenaltyReturnTypeOnItsOwnLine: 10000 + +PointerAlignment: Left + +ReflowComments: false + +SortIncludes: true +SortUsingDeclarations: true + +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false + +Standard: Cpp11 + +StatementMacros: + - Q_UNUSED + +TabWidth: 4 + +TypenameMacros: + - STACK_OF + - LIST + - LIST_ENTRY + +UseTab: ForContinuationAndIndentation \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 433d2746..ab4d1bf5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,10 +75,14 @@ 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 remove_old_python_dir + DEPENDS "${TARGET_NAME}" remove_old_python_dir ) add_custom_target( copy_misc_files ALL COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_LIST_DIR}/README.md" "${CMAKE_CURRENT_LIST_DIR}/LICENSE" "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}" COMMENT "Copying misc. files to ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}" -) \ No newline at end of file + DEPENDS "${TARGET_NAME}" +) + +enable_testing() +add_subdirectory(test) \ No newline at end of file diff --git a/README.md b/README.md index bcd09857..54440d52 100644 --- a/README.md +++ b/README.md @@ -62,13 +62,13 @@ python3 setup.py sdist The output gets written to `B-ASIC/dist/b-asic-<version>.tar.gz`. #### Installation (Binary distribution) -In `B-ASIC`: +In `B-ASIC/dist`: ``` python3 -m pip install b_asic-<version>-<python_tag>-<abi_tag>-<platform_tag>.whl ``` #### Installation (Source distribution) -In `B-ASIC`: +In `B-ASIC/dist`: ``` python3 -m pip install b-asic-<version>.tar.gz ``` diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 00000000..c9fd7f20 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.13) + +project("B-ASIC-test") + +add_executable( + test-main + "${CMAKE_CURRENT_SOURCE_DIR}/test_main.cpp" +) +add_test(NAME test-main COMMAND test-main) \ No newline at end of file diff --git a/test/test_main.cpp b/test/test_main.cpp new file mode 100644 index 00000000..30ea92f7 --- /dev/null +++ b/test/test_main.cpp @@ -0,0 +1,5 @@ +// TODO: Tests + +int main() { + return 0; +} \ No newline at end of file -- GitLab From 7175f248593299ec06353bbf2ab5f719fc2419b8 Mon Sep 17 00:00:00 2001 From: Jacob Wahlman <jacwa448@student.liu.se> Date: Tue, 25 Feb 2020 14:41:29 +0100 Subject: [PATCH 18/50] Fixed invalid abstract method override --- b_asic/basic_operation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b_asic/basic_operation.py b/b_asic/basic_operation.py index 6810a9a7..d5d03045 100644 --- a/b_asic/basic_operation.py +++ b/b_asic/basic_operation.py @@ -39,7 +39,7 @@ class BasicOperation(Operation): """ pass - def id(self) -> OperationId: + def identifier(self) -> OperationId: return self._identifier def inputs(self) -> List[InputPort]: -- GitLab From efcd8394cc64fbd490899e8a2ff559301ecfa56f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20H=C3=A4rnqvist?= <ivaha717@student.liu.se> Date: Tue, 25 Feb 2020 16:35:57 +0100 Subject: [PATCH 19/50] fix test setup --- .gitignore | 4 +++- CMakeLists.txt | 11 +-------- README.md | 46 ++++++++++++++++++++++--------------- test/CMakeLists.txt | 9 -------- {tests => test}/__init__.py | 0 test/test_main.cpp | 5 ---- test/test_port.py | 9 ++++++++ tests/test_port.py | 10 -------- 8 files changed, 41 insertions(+), 53 deletions(-) delete mode 100644 test/CMakeLists.txt rename {tests => test}/__init__.py (100%) delete mode 100644 test/test_main.cpp create mode 100644 test/test_port.py delete mode 100644 tests/test_port.py diff --git a/.gitignore b/.gitignore index 36987299..0bdf5476 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,6 @@ $RECYCLE.BIN/ *.stackdump [Dd]esktop.ini *.egg-info -__pycache__/ \ No newline at end of file +__pycache__/ +env/ +venv/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index ab4d1bf5..bcd7af2e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,13 +76,4 @@ add_custom_target( 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 -) -add_custom_target( - copy_misc_files ALL - COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_LIST_DIR}/README.md" "${CMAKE_CURRENT_LIST_DIR}/LICENSE" "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}" - COMMENT "Copying misc. files to ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}" - DEPENDS "${TARGET_NAME}" -) - -enable_testing() -add_subdirectory(test) \ No newline at end of file +) \ No newline at end of file diff --git a/README.md b/README.md index 4552c3b6..58025fbb 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,10 @@ # B-ASIC - Better ASIC Toolbox B-ASIC is an ASIC toolbox for Python 3 that simplifies circuit design and optimization. -## Prerequisites +## Development +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+ @@ -16,9 +19,6 @@ The following packages are required in order to build the library: * numpy * pyside2/pyqt5 -## Development -How to build and debug the library during development. - ### Using CMake directly How to build using CMake. @@ -64,13 +64,34 @@ The output gets written to `B-ASIC/dist/b-asic-<version>.tar.gz`. #### Installation (Binary distribution) In `B-ASIC/dist`: ``` -python3 -m pip install b_asic-<version>-<python_tag>-<abi_tag>-<platform_tag>.whl +pip install b_asic-<version>-<python_tag>-<abi_tag>-<platform_tag>.whl ``` #### Installation (Source distribution) In `B-ASIC/dist`: ``` -python3 -m pip install b-asic-<version>.tar.gz +pip install b-asic-<version>.tar.gz +``` + +### Running tests +How to run the tests using pytest in a virtual environment. + +#### Linux/OS X +In `B-ASIC`: +``` +python3 -m venv env +source env/bin/activate +pip install . +pytest +``` + +#### Windows +In `B-ASIC` (as admin): +``` +python3 -m venv env +.\env\Scripts\activate.bat +pip install . +pytest ``` ## Usage @@ -78,7 +99,7 @@ How to build and use the library as a user. ### Installation ``` -python3 -m pip install b_asic +pip install b_asic ``` ### Importing @@ -88,17 +109,6 @@ python3 >>> help(asic) ``` -## Running tests -### pytest -Run all tests against installed version off b_asic using a virtual enviroment -In `B-ASIC`: -``` -python3 -m venv .venv -source .venv/bin/activate -pip install . -pytest -``` - ## License B-ASIC is distributed under the MIT license. See the included LICENSE file for more information. diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt deleted file mode 100644 index c9fd7f20..00000000 --- a/test/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -cmake_minimum_required(VERSION 3.13) - -project("B-ASIC-test") - -add_executable( - test-main - "${CMAKE_CURRENT_SOURCE_DIR}/test_main.cpp" -) -add_test(NAME test-main COMMAND test-main) \ No newline at end of file diff --git a/tests/__init__.py b/test/__init__.py similarity index 100% rename from tests/__init__.py rename to test/__init__.py diff --git a/test/test_main.cpp b/test/test_main.cpp deleted file mode 100644 index 30ea92f7..00000000 --- a/test/test_main.cpp +++ /dev/null @@ -1,5 +0,0 @@ -// TODO: Tests - -int main() { - return 0; -} \ No newline at end of file diff --git a/test/test_port.py b/test/test_port.py new file mode 100644 index 00000000..62e17ea5 --- /dev/null +++ b/test/test_port.py @@ -0,0 +1,9 @@ +import b_asic +import pytest + +@pytest.fixture +def outp_port(): + return b_asic.InputPort(0) + +def test_port(outp_port): + assert outp_port.signals() == [] \ No newline at end of file diff --git a/tests/test_port.py b/tests/test_port.py deleted file mode 100644 index 498233bb..00000000 --- a/tests/test_port.py +++ /dev/null @@ -1,10 +0,0 @@ -from b_asic import InputPort -import pytest - -@pytest.fixture -def outp_port(): - return InputPort(0) - - -def test_port(outp_port): - assert outp_port.signals() == [] -- GitLab From faf0f394dd6973e86fcb914c8c669ab463f2caf4 Mon Sep 17 00:00:00 2001 From: Kevin Scott <kevsc634@student.liu.se> Date: Wed, 26 Feb 2020 15:57:24 +0100 Subject: [PATCH 20/50] Updated README, added package for test coverage and added some test files --- README.md | 5 +++++ setup.py | 3 ++- test/test_inputport.py | 28 ++++++++++++++++++++++++++++ test/test_outputport.py | 28 ++++++++++++++++++++++++++++ test/test_port.py | 32 +++++++++++++++++++++++++++----- test/test_signal.py | 28 ++++++++++++++++++++++++++++ 6 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 test/test_inputport.py create mode 100644 test/test_outputport.py create mode 100644 test/test_signal.py diff --git a/README.md b/README.md index 58025fbb..bb71c4d7 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,11 @@ pip install . pytest ``` +#### Test with coverage +``` +pytest --cov=b_asic --cov-report html test +``` + ## Usage How to build and use the library as a user. diff --git a/setup.py b/setup.py index 0b591cbb..cfe8605f 100644 --- a/setup.py +++ b/setup.py @@ -71,7 +71,8 @@ setuptools.setup( "pybind11>=2.3.0", "numpy", "install_qt_binding", - "pytest==5.3.4" + "pytest==5.3.4", + "pytest-cov==2.8.1" ], packages = ["b_asic"], ext_modules = [CMakeExtension("b_asic")], diff --git a/test/test_inputport.py b/test/test_inputport.py new file mode 100644 index 00000000..0cb3cab9 --- /dev/null +++ b/test/test_inputport.py @@ -0,0 +1,28 @@ +""" +B-ASIC test suite for Inputport +TODO: More info +""" +from b_asic import InputPort, Signal, SignalSource, SignalDestination, Addition +import pytest + +@pytest.fixture +def signal(): + source = SignalSource(Addition(0), 1) + dest = SignalDestination(Addition(1),2) + return Signal(source, dest) + +def test_connect_multple_signals(signal): + """ + make sure we can only connect one signal to an input port + """ + inp_port = InputPort(0) + inp_port.connect(signal) + + # create new signal + source = SignalSource(Addition(0), 3) + dest = SignalDestination(Addition(1),4) + new_signal = Signal(source, dest) + + inp_port.connect(new_signal) + inp_port.signal_count() == 1 + assert inp_port.signals() == [new_signal] \ No newline at end of file diff --git a/test/test_outputport.py b/test/test_outputport.py new file mode 100644 index 00000000..35331670 --- /dev/null +++ b/test/test_outputport.py @@ -0,0 +1,28 @@ +""" +B-ASIC test suite for InputPort +TODO: More info +""" +from b_asic import OutputPort, Signal, SignalSource, SignalDestination, Addition +import pytest + +@pytest.fixture +def signal(): + source = SignalSource(Addition(0), 1) + dest = SignalDestination(Addition(1),2) + return Signal(source, dest) + +def test_connect_multiple_signals(signal): + """ + make sure we can connect multiple signals to an output port + """ + outp_port = OutputPort(0) + outp_port.connect(signal) + + # create new signal + source = SignalSource(Addition(0), 3) + dest = SignalDestination(Addition(1),4) + new_signal = Signal(source, dest) + + outp_port.connect(new_signal) + outp_port.signal_count() == 2 + assert outp_port.signals() == [signal, new_signal] \ No newline at end of file diff --git a/test/test_port.py b/test/test_port.py index 62e17ea5..f4e039a9 100644 --- a/test/test_port.py +++ b/test/test_port.py @@ -1,9 +1,31 @@ -import b_asic +""" +B-ASIC test suite for Port interface, place all general test cases for abstract class Port here +TODO: More info +""" + +from b_asic import InputPort, OutputPort, Signal, SignalSource, SignalDestination, Addition import pytest @pytest.fixture -def outp_port(): - return b_asic.InputPort(0) +def signal(): + source = SignalSource(Addition(0), 1) + dest = SignalDestination(Addition(1),2) + return Signal(source, dest) + +def test_connect_one_signal_to_port(signal): + port = InputPort(0) + port.connect(signal) + assert len(port.signals()) == 1 + assert port.signal() == signal + +def test_change_port_signal(): + source = SignalSource(Addition(0), 1) + dest = SignalDestination(Addition(1),2) + signal1 = Signal(source, dest) + signal2 = Signal(source, dest) -def test_port(outp_port): - assert outp_port.signals() == [] \ No newline at end of file + port = InputPort(0) + port.connect(signal1) + assert port.signal() == signal1 + port.connect(signal2) + assert port.signal() == signal2 \ No newline at end of file diff --git a/test/test_signal.py b/test/test_signal.py new file mode 100644 index 00000000..35d51eb5 --- /dev/null +++ b/test/test_signal.py @@ -0,0 +1,28 @@ +from b_asic import Signal, SignalSource, SignalDestination +from b_asic.core_operations import Addition +import pytest + +# TODO mock operation +# Use port index + +@pytest.fixture +def operation(): + return Addition(0) + +@pytest.fixture +def signal_dest(operation): + return SignalDestination(operation, 0) + +@pytest.fixture +def signal_src(operation): + return SignalSource(operation, 0) + +def test_construct_source_signal(operation): + s = SignalSource(operation, 0) + assert True + + +def test_construct_signal(): + s = Signal(signal_src, signal_dest) + assert True + -- GitLab From 541b13945e439d8d5a1007a64da47c572310f471 Mon Sep 17 00:00:00 2001 From: Kevin Scott <kevsc634@student.liu.se> Date: Thu, 27 Feb 2020 11:57:12 +0100 Subject: [PATCH 21/50] fixed error when running tests and restructured tests --- test/conftest.py | 2 ++ test/fixtures/__init__.py | 0 test/fixtures/signal.py | 20 ++++++++++++++++++++ test/port/__init__.py | 0 test/port/test_inputport.py | 23 +++++++++++++++++++++++ test/port/test_outputport.py | 18 ++++++++++++++++++ test/{ => port}/test_port.py | 10 ++-------- test/test_inputport.py | 28 ---------------------------- test/test_outputport.py | 28 ---------------------------- test/test_signal.py | 28 ---------------------------- 10 files changed, 65 insertions(+), 92 deletions(-) create mode 100644 test/conftest.py create mode 100644 test/fixtures/__init__.py create mode 100644 test/fixtures/signal.py create mode 100644 test/port/__init__.py create mode 100644 test/port/test_inputport.py create mode 100644 test/port/test_outputport.py rename test/{ => port}/test_port.py (73%) delete mode 100644 test/test_inputport.py delete mode 100644 test/test_outputport.py delete mode 100644 test/test_signal.py diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 00000000..986af94c --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,2 @@ +import pytest +from test.fixtures.signal import * \ No newline at end of file diff --git a/test/fixtures/__init__.py b/test/fixtures/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/signal.py b/test/fixtures/signal.py new file mode 100644 index 00000000..5fbdcf2b --- /dev/null +++ b/test/fixtures/signal.py @@ -0,0 +1,20 @@ +import pytest +from b_asic import Signal, SignalSource, SignalDestination, Addition + +""" +Use a fixture for initializing objects and pass them as argument to a test function +""" +@pytest.fixture +def signal(): + source = SignalSource(Addition(0), 1) + dest = SignalDestination(Addition(1), 2) + return Signal(0, source, dest) + +@pytest.fixture +def signals(): + ret = [] + for i in range(0,3): + source = SignalSource(Addition(0), 1) + dest = SignalDestination(Addition(1), 2) + ret.append(Signal(i, source, dest)) + return ret \ No newline at end of file diff --git a/test/port/__init__.py b/test/port/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/port/test_inputport.py b/test/port/test_inputport.py new file mode 100644 index 00000000..d761900a --- /dev/null +++ b/test/port/test_inputport.py @@ -0,0 +1,23 @@ +""" +B-ASIC test suite for Inputport +""" + +# import module we are testing +from b_asic import InputPort + +# import dependencies +from b_asic import Signal, SignalSource, SignalDestination, Addition + +import pytest + +def test_connect_multiple_signals(signals): + """ + test if only one signal can connect to an input port + """ + inp_port = InputPort(0) + + for s in signals: + inp_port.connect(s) + + assert inp_port.signal_count() == 1 + assert inp_port.signals()[0] == signals[-1] \ No newline at end of file diff --git a/test/port/test_outputport.py b/test/port/test_outputport.py new file mode 100644 index 00000000..5f7b8f49 --- /dev/null +++ b/test/port/test_outputport.py @@ -0,0 +1,18 @@ +""" +B-ASIC test suite for InputPort +TODO: More info +""" +from b_asic import OutputPort, Signal, SignalSource, SignalDestination, Addition +import pytest + +def test_connect_multiple_signals(signals): + """ + test if multiple signals can connect to an output port + """ + outp_port = OutputPort(0) + + for s in signals: + outp_port.connect(s) + + assert outp_port.signal_count() == 3 + assert outp_port.signals() == signals \ No newline at end of file diff --git a/test/test_port.py b/test/port/test_port.py similarity index 73% rename from test/test_port.py rename to test/port/test_port.py index f4e039a9..56cb9be2 100644 --- a/test/test_port.py +++ b/test/port/test_port.py @@ -1,16 +1,10 @@ """ B-ASIC test suite for Port interface, place all general test cases for abstract class Port here -TODO: More info """ from b_asic import InputPort, OutputPort, Signal, SignalSource, SignalDestination, Addition import pytest -@pytest.fixture -def signal(): - source = SignalSource(Addition(0), 1) - dest = SignalDestination(Addition(1),2) - return Signal(source, dest) def test_connect_one_signal_to_port(signal): port = InputPort(0) @@ -21,8 +15,8 @@ def test_connect_one_signal_to_port(signal): def test_change_port_signal(): source = SignalSource(Addition(0), 1) dest = SignalDestination(Addition(1),2) - signal1 = Signal(source, dest) - signal2 = Signal(source, dest) + signal1 = Signal(1, source, dest) + signal2 = Signal(2, source, dest) port = InputPort(0) port.connect(signal1) diff --git a/test/test_inputport.py b/test/test_inputport.py deleted file mode 100644 index 0cb3cab9..00000000 --- a/test/test_inputport.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -B-ASIC test suite for Inputport -TODO: More info -""" -from b_asic import InputPort, Signal, SignalSource, SignalDestination, Addition -import pytest - -@pytest.fixture -def signal(): - source = SignalSource(Addition(0), 1) - dest = SignalDestination(Addition(1),2) - return Signal(source, dest) - -def test_connect_multple_signals(signal): - """ - make sure we can only connect one signal to an input port - """ - inp_port = InputPort(0) - inp_port.connect(signal) - - # create new signal - source = SignalSource(Addition(0), 3) - dest = SignalDestination(Addition(1),4) - new_signal = Signal(source, dest) - - inp_port.connect(new_signal) - inp_port.signal_count() == 1 - assert inp_port.signals() == [new_signal] \ No newline at end of file diff --git a/test/test_outputport.py b/test/test_outputport.py deleted file mode 100644 index 35331670..00000000 --- a/test/test_outputport.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -B-ASIC test suite for InputPort -TODO: More info -""" -from b_asic import OutputPort, Signal, SignalSource, SignalDestination, Addition -import pytest - -@pytest.fixture -def signal(): - source = SignalSource(Addition(0), 1) - dest = SignalDestination(Addition(1),2) - return Signal(source, dest) - -def test_connect_multiple_signals(signal): - """ - make sure we can connect multiple signals to an output port - """ - outp_port = OutputPort(0) - outp_port.connect(signal) - - # create new signal - source = SignalSource(Addition(0), 3) - dest = SignalDestination(Addition(1),4) - new_signal = Signal(source, dest) - - outp_port.connect(new_signal) - outp_port.signal_count() == 2 - assert outp_port.signals() == [signal, new_signal] \ No newline at end of file diff --git a/test/test_signal.py b/test/test_signal.py deleted file mode 100644 index 35d51eb5..00000000 --- a/test/test_signal.py +++ /dev/null @@ -1,28 +0,0 @@ -from b_asic import Signal, SignalSource, SignalDestination -from b_asic.core_operations import Addition -import pytest - -# TODO mock operation -# Use port index - -@pytest.fixture -def operation(): - return Addition(0) - -@pytest.fixture -def signal_dest(operation): - return SignalDestination(operation, 0) - -@pytest.fixture -def signal_src(operation): - return SignalSource(operation, 0) - -def test_construct_source_signal(operation): - s = SignalSource(operation, 0) - assert True - - -def test_construct_signal(): - s = Signal(signal_src, signal_dest) - assert True - -- GitLab From 3dbd937da8ac0695430851f84441525708843e1e Mon Sep 17 00:00:00 2001 From: Jacob Wahlman <jacwa448@student.liu.se> Date: Mon, 2 Mar 2020 13:34:15 +0100 Subject: [PATCH 22/50] Move OperationID from operation to signal_flow_graph and change sfg operations datastructure to a dict --- b_asic/basic_operation.py | 18 ++-- b_asic/core_operations.py | 35 +++++--- b_asic/graph_id.py | 30 +++++++ b_asic/operation.py | 24 +++--- b_asic/signal.py | 13 +-- b_asic/signal_flow_graph.py | 65 +++++++++++--- b_asic/simulation.py | 3 +- b_asic/traverse_tree.py | 54 ++++++++++++ test/fixtures/signal.py | 14 +-- test/graph_id/conftest.py | 1 + test/graph_id/test_graph_id_generator.py | 29 +++++++ test/port/test_port.py | 8 +- test/signal_flow_graph/conftest.py | 1 + .../test_signal_flow_graph.py | 3 + test/traverse/test_traverse_tree.py | 85 +++++++++++++++++++ 15 files changed, 314 insertions(+), 69 deletions(-) create mode 100644 b_asic/graph_id.py create mode 100644 b_asic/traverse_tree.py create mode 100644 test/graph_id/conftest.py create mode 100644 test/graph_id/test_graph_id_generator.py create mode 100644 test/signal_flow_graph/conftest.py create mode 100644 test/signal_flow_graph/test_signal_flow_graph.py create mode 100644 test/traverse/test_traverse_tree.py diff --git a/b_asic/basic_operation.py b/b_asic/basic_operation.py index d5d03045..4f7b426b 100644 --- a/b_asic/basic_operation.py +++ b/b_asic/basic_operation.py @@ -5,7 +5,7 @@ TODO: More info. from b_asic.port import InputPort, OutputPort from b_asic.signal import SignalSource, SignalDestination -from b_asic.operation import OperationId, Operation +from b_asic.operation import Operation from b_asic.simulation import SimulationState, OperationState from abc import ABC, abstractmethod from typing import List, Dict, Optional, Any @@ -18,16 +18,14 @@ class BasicOperation(Operation): TODO: More info. """ - _identifier: OperationId _input_ports: List[InputPort] _output_ports: List[OutputPort] _parameters: Dict[str, Optional[Any]] - def __init__(self, identifier: OperationId): + def __init__(self): """ Construct a BasicOperation. """ - self._identifier = identifier self._input_ports = [] self._output_ports = [] self._parameters = {} @@ -39,9 +37,6 @@ class BasicOperation(Operation): """ pass - def identifier(self) -> OperationId: - return self._identifier - def inputs(self) -> List[InputPort]: return self._input_ports.copy() @@ -102,4 +97,13 @@ class BasicOperation(Operation): return results return [self] + @property + def neighbours(self) -> List[Operation]: + neighbours: List[Operation] = [] + for port in self._output_ports + self._input_ports: + for signal in port.signals(): + neighbours += [signal.source.operation, signal.destination.operation] + + return neighbours + # TODO: More stuff. diff --git a/b_asic/core_operations.py b/b_asic/core_operations.py index c766d06c..bca344cf 100644 --- a/b_asic/core_operations.py +++ b/b_asic/core_operations.py @@ -4,8 +4,9 @@ TODO: More info. """ from b_asic.port import InputPort, OutputPort -from b_asic.operation import OperationId, Operation +from b_asic.operation import Operation from b_asic.basic_operation import BasicOperation +from b_asic.graph_id import GraphIDType from numbers import Number @@ -15,7 +16,7 @@ class Input(Operation): TODO: More info. """ - # TODO: Implement. + # TODO: Implement all functions. pass @@ -25,17 +26,19 @@ class Constant(BasicOperation): TODO: More info. """ - def __init__(self, identifier: OperationId, value: Number): + def __init__(self, value: Number): """ Construct a Constant. """ - super().__init__(identifier) - self._output_ports = [OutputPort()] # TODO: Generate appropriate ID for ports. + super().__init__() + self._output_ports = [OutputPort(1)] # TODO: Generate appropriate ID for ports. self._parameters["value"] = value def evaluate(self, inputs: list) -> list: return [self.param("value")] + def get_op_name(self) -> GraphIDType: + return "const" class Addition(BasicOperation): """ @@ -43,17 +46,20 @@ class Addition(BasicOperation): TODO: More info. """ - def __init__(self, identifier: OperationId): + def __init__(self): """ Construct an Addition. """ - super().__init__(identifier) - self._input_ports = [InputPort(), InputPort()] # TODO: Generate appropriate ID for ports. - self._output_ports = [OutputPort()] # TODO: Generate appropriate ID for ports. + super().__init__() + self._input_ports = [InputPort(1), InputPort(1)] # TODO: Generate appropriate ID for ports. + self._output_ports = [OutputPort(1)] # TODO: Generate appropriate ID for ports. def evaluate(self, inputs: list) -> list: return [inputs[0] + inputs[1]] + def get_op_name(self) -> GraphIDType: + return "add" + class ConstantMultiplication(BasicOperation): """ @@ -61,16 +67,19 @@ class ConstantMultiplication(BasicOperation): TODO: More info. """ - def __init__(self, identifier: OperationId, coefficient: Number): + def __init__(self, coefficient: Number): """ Construct a ConstantMultiplication. """ - super().__init__(identifier) - self._input_ports = [InputPort()] # TODO: Generate appropriate ID for ports. - self._output_ports = [OutputPort()] # TODO: Generate appropriate ID for ports. + super().__init__() + self._input_ports = [InputPort(1)] # TODO: Generate appropriate ID for ports. + self._output_ports = [OutputPort(1)] # TODO: Generate appropriate ID for ports. self._parameters["coefficient"] = coefficient def evaluate(self, inputs: list) -> list: return [inputs[0] * self.param("coefficient")] + def get_op_name(self) -> GraphIDType: + return "const_mul" + # TODO: More operations. diff --git a/b_asic/graph_id.py b/b_asic/graph_id.py new file mode 100644 index 00000000..3f25f513 --- /dev/null +++ b/b_asic/graph_id.py @@ -0,0 +1,30 @@ +""" +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, Union, 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: + """ + Returns 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 731822f1..f02cd700 100644 --- a/b_asic/operation.py +++ b/b_asic/operation.py @@ -10,9 +10,7 @@ from typing import NewType, List, Dict, Optional, Any, TYPE_CHECKING if TYPE_CHECKING: from b_asic.port import InputPort, OutputPort from b_asic.simulation import SimulationState - -OperationId = NewType("OperationId", int) - + from b_asic.graph_id import GraphIDType class Operation(ABC): """ @@ -20,14 +18,6 @@ class Operation(ABC): TODO: More info. """ - @abstractmethod - def identifier(self) -> OperationId: - """ - Get the unique identifier. - TODO: Move id info to SFG, remove id class members. - """ - pass - @abstractmethod def inputs(self) -> "List[InputPort]": """ @@ -109,5 +99,17 @@ class Operation(ABC): """ pass + @abstractmethod + def get_op_name(self) -> "GraphIDType": + """Returns a string representing the operation name of the operation.""" + pass + + @abstractmethod + def neighbours(self) -> "List[Operation]": + """ + Return all operations that are connected by signals to this operation. + If no neighbours are found this returns an empty list + """ + # TODO: More stuff. diff --git a/b_asic/signal.py b/b_asic/signal.py index 4fac563f..6ef55c8d 100644 --- a/b_asic/signal.py +++ b/b_asic/signal.py @@ -6,9 +6,6 @@ TODO: More info. from b_asic.operation import Operation from typing import NewType -SignalId = NewType("SignalId", int) - - class SignalSource: """ Handle to a signal source. @@ -50,22 +47,14 @@ class Signal: A connection between two operations consisting of a source and destination handle. TODO: More info. """ - _identifier: SignalId source: SignalSource destination: SignalDestination - def __init__(self, identifier: SignalId, source: SignalSource, destination: SignalDestination): + def __init__(self, source: SignalSource, destination: SignalDestination): """ Construct a Signal. """ - self._identifier = identifier self.source = source self.destination = destination - def identifier(self) -> SignalId: - """ - Get the unique identifier. - """ - return self._identifier - # TODO: More stuff. diff --git a/b_asic/signal_flow_graph.py b/b_asic/signal_flow_graph.py index f9671636..9d31b04b 100644 --- a/b_asic/signal_flow_graph.py +++ b/b_asic/signal_flow_graph.py @@ -3,12 +3,13 @@ B-ASIC Signal Flow Graph Module. TODO: More info. """ -from b_asic.operation import OperationId, Operation +from b_asic.operation import Operation from b_asic.basic_operation import BasicOperation -from b_asic.signal import SignalSource, SignalDestination +from b_asic.signal import Signal, SignalSource, SignalDestination from b_asic.simulation import SimulationState, OperationState -from typing import List +from b_asic.graph_id import GraphIDGenerator, GraphID +from typing import List, Dict, Union, Optional class SFG(BasicOperation): """ @@ -16,22 +17,60 @@ class SFG(BasicOperation): TODO: More info. """ - _operations: List[Operation] + _graph_objects_by_id: Dict[GraphID, Union[Operation, Signal]] + _graph_id_generator: GraphIDGenerator - def __init__(self, identifier: OperationId, input_destinations: List[SignalDestination], output_sources: List[SignalSource]): - """ - Construct a SFG. - """ - super().__init__(identifier) + def __init__(self, input_destinations: List[SignalDestination], output_sources: List[SignalSource]): + """Constructs an SFG.""" + super().__init__() # TODO: Allocate input/output ports with appropriate IDs. - self._operations = [] + + self._graph_objects_by_id = dict # Map Operation ID to Operation objects + self._graph_id_generator = GraphIDGenerator() + # TODO: Traverse the graph between the inputs/outputs and add to self._operations. # TODO: Connect ports with signals with appropriate IDs. def evaluate(self, inputs: list) -> list: return [] # TODO: Implement - def split(self) -> List[Operation]: - return self._operations + def add_operation(self, operation: Operation) -> GraphID: + """Adds the entered operation to the SFG's dictionary of graph objects and + returns a generated GraphID for it. + + Keyword arguments: + operation: Operation to add to the graph. + """ + return self._add_graph_obj(operation, operation.get_op_name()) + + + def add_signal(self, signal: Signal) -> GraphID: + """Adds the entered signal to the SFG's dictionary of graph objects and returns + a generated GraphID for it. + + Keyword argumentst: + signal: Signal to add to the graph. + """ + return self._add_graph_obj(signal, 'sig') + + + def find_by_id(self, graph_id: GraphID) -> Optional[Operation]: + """Finds a graph object based on the entered Graph ID and returns it. If no graph + object with the entered ID was found then returns None. + + Keyword arguments: + graph_id: Graph ID of the wanted object. + """ + if graph_id in self._graph_objects_by_id: + return self._graph_objects_by_id[graph_id] + else: + return None + + + + def _add_graph_obj(self, obj: Union[Operation, Signal], operation_id_type: str): + graph_id = self._graph_id_generator.get_next_id(operation_id_type) + self._graph_objects_by_id[graph_id] = obj + return graph_id + - # TODO: More stuff. diff --git a/b_asic/simulation.py b/b_asic/simulation.py index e219445b..d3d3aaf6 100644 --- a/b_asic/simulation.py +++ b/b_asic/simulation.py @@ -3,7 +3,6 @@ B-ASIC Simulation Module. TODO: More info. """ -from b_asic.operation import OperationId from numbers import Number from typing import List, Dict @@ -31,7 +30,7 @@ class SimulationState: TODO: More info. """ - operation_states: Dict[OperationId, OperationState] + # operation_states: Dict[OperationId, OperationState] iteration: int def __init__(self): diff --git a/b_asic/traverse_tree.py b/b_asic/traverse_tree.py new file mode 100644 index 00000000..024a542d --- /dev/null +++ b/b_asic/traverse_tree.py @@ -0,0 +1,54 @@ +""" +B-ASIC Operation Tree Traversing Module. +TODO: + - Get a first operation or? an entire operation tree + - For each start point, follow it to the next operation from it's out port. + - If we are searching for a specific operation end. + - If we are searching for a specific type of operation add the operation to a list and continue. + - When we no more out ports can be traversed return results and end. +""" + +from typing import List, Optional +from collections import deque + +from b_asic.operation import Operation + + +class Traverse: + """Traverse operation tree. + TODO: + - More info. + - Check if a datastructure other than list suits better as return value. + - Implement the type check for operation. + """ + + def __init__(self, operation: Operation): + """Construct a TraverseTree.""" + self._initial_operation = operation + + def _breadth_first_search(self, start: Operation) -> List[Operation]: + """Use breadth first search to traverse the operation tree.""" + visited: List[Operation] = [start] + queue = deque([start]) + while queue: + operation = queue.popleft() + for n_operation in operation.neighbours: + if n_operation not in visited: + visited.append(n_operation) + queue.append(n_operation) + + return visited + + def traverse(self, type_: Optional[Operation] = None) -> List[Operation]: + """Traverse the the operation tree and return operation where type matches. + If the type is None then return the entire tree. + + Keyword arguments: + type_-- the operation type to search for (default None) + """ + + operations: List[Operation] = self._breadth_first_search(self._initial_operation) + if type_ is not None: + operations = [oper for oper in operations if isinstance(oper, type_)] + + return operations diff --git a/test/fixtures/signal.py b/test/fixtures/signal.py index 5fbdcf2b..64b96f55 100644 --- a/test/fixtures/signal.py +++ b/test/fixtures/signal.py @@ -6,15 +6,15 @@ Use a fixture for initializing objects and pass them as argument to a test funct """ @pytest.fixture def signal(): - source = SignalSource(Addition(0), 1) - dest = SignalDestination(Addition(1), 2) - return Signal(0, source, dest) + source = SignalSource(Addition(), 1) + dest = SignalDestination(Addition(), 2) + return Signal(source, dest) @pytest.fixture def signals(): ret = [] - for i in range(0,3): - source = SignalSource(Addition(0), 1) - dest = SignalDestination(Addition(1), 2) - ret.append(Signal(i, source, dest)) + for _ in range(0,3): + source = SignalSource(Addition(), 1) + dest = SignalDestination(Addition(), 2) + ret.append(Signal(source, dest)) return ret \ No newline at end of file diff --git a/test/graph_id/conftest.py b/test/graph_id/conftest.py new file mode 100644 index 00000000..5871ed8e --- /dev/null +++ b/test/graph_id/conftest.py @@ -0,0 +1 @@ +import pytest diff --git a/test/graph_id/test_graph_id_generator.py b/test/graph_id/test_graph_id_generator.py new file mode 100644 index 00000000..7aeb6cad --- /dev/null +++ b/test/graph_id/test_graph_id_generator.py @@ -0,0 +1,29 @@ +""" +B-ASIC test suite for graph id generator. +""" + +from b_asic.graph_id import GraphIDGenerator, GraphID + +import pytest + +def test_empty_string_generator(): + """Test the graph id generator for an empty string type.""" + graph_id_generator = GraphIDGenerator() + assert graph_id_generator.get_next_id("") == "1" + assert graph_id_generator.get_next_id("") == "2" + + +def test_normal_string_generator(): + """"Test the graph id generator for a normal string type.""" + graph_id_generator = GraphIDGenerator() + assert graph_id_generator.get_next_id("add") == "add1" + assert graph_id_generator.get_next_id("add") == "add2" + +def test_different_strings_generator(): + """Test the graph id generator for different strings.""" + graph_id_generator = GraphIDGenerator() + 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" + \ No newline at end of file diff --git a/test/port/test_port.py b/test/port/test_port.py index 56cb9be2..7e1fc9b7 100644 --- a/test/port/test_port.py +++ b/test/port/test_port.py @@ -13,10 +13,10 @@ def test_connect_one_signal_to_port(signal): assert port.signal() == signal def test_change_port_signal(): - source = SignalSource(Addition(0), 1) - dest = SignalDestination(Addition(1),2) - signal1 = Signal(1, source, dest) - signal2 = Signal(2, source, dest) + source = SignalSource(Addition, 1) + dest = SignalDestination(Addition,2) + signal1 = Signal(source, dest) + signal2 = Signal(source, dest) port = InputPort(0) port.connect(signal1) diff --git a/test/signal_flow_graph/conftest.py b/test/signal_flow_graph/conftest.py new file mode 100644 index 00000000..5871ed8e --- /dev/null +++ b/test/signal_flow_graph/conftest.py @@ -0,0 +1 @@ +import pytest diff --git a/test/signal_flow_graph/test_signal_flow_graph.py b/test/signal_flow_graph/test_signal_flow_graph.py new file mode 100644 index 00000000..921e8906 --- /dev/null +++ b/test/signal_flow_graph/test_signal_flow_graph.py @@ -0,0 +1,3 @@ +from b_asic.signal_flow_graph import SFG +from b_asic.core_operations import Addition, Constant +from b_asic.signal import Signal diff --git a/test/traverse/test_traverse_tree.py b/test/traverse/test_traverse_tree.py new file mode 100644 index 00000000..d218a6e7 --- /dev/null +++ b/test/traverse/test_traverse_tree.py @@ -0,0 +1,85 @@ +""" +TODO: + - Rewrite to more clean code, not so repetitive + - Update when signals and id's has been merged. +""" + +from b_asic.core_operations import Constant, Addition +from b_asic.signal import Signal, SignalSource, SignalDestination +from b_asic.port import InputPort, OutputPort +from b_asic.traverse_tree import Traverse + +import pytest + +@pytest.fixture +def operation(): + return Constant(2) + +def create_operation(_type, dest_oper, index, **kwargs): + oper = _type(**kwargs) + oper_signal_source = SignalSource(oper, 0) + oper_signal_dest = SignalDestination(dest_oper, index) + oper_signal = Signal(oper_signal_source, oper_signal_dest) + oper._output_ports[0].connect(oper_signal) + dest_oper._input_ports[index].connect(oper_signal) + return oper + +@pytest.fixture +def operation_tree(): + add_oper = Addition() + + const_oper = create_operation(Constant, add_oper, 0, value=2) + const_oper_2 = create_operation(Constant, add_oper, 1, value=3) + + return add_oper + +@pytest.fixture +def large_operation_tree(): + add_oper = Addition() + add_oper_2 = Addition() + + const_oper = create_operation(Constant, add_oper, 0, value=2) + const_oper_2 = create_operation(Constant, add_oper, 1, value=3) + + const_oper_3 = create_operation(Constant, add_oper_2, 0, value=4) + const_oper_4 = create_operation(Constant, add_oper_2, 1, value=5) + + add_oper_3 = Addition() + add_oper_signal_source = SignalSource(add_oper, 0) + add_oper_signal_dest = SignalDestination(add_oper_3, 0) + add_oper_signal = Signal(add_oper_signal_source, add_oper_signal_dest) + add_oper._output_ports[0].connect(add_oper_signal) + add_oper_3._input_ports[0].connect(add_oper_signal) + + add_oper_2_signal_source = SignalSource(add_oper_2, 0) + add_oper_2_signal_dest = SignalDestination(add_oper_3, 1) + add_oper_2_signal = Signal(add_oper_2_signal_source, add_oper_2_signal_dest) + add_oper_2._output_ports[0].connect(add_oper_2_signal) + add_oper_3._input_ports[1].connect(add_oper_2_signal) + return const_oper + +def test_traverse_single_tree(operation): + traverse = Traverse(operation) + assert traverse.traverse() == [operation] + +def test_traverse_tree(operation_tree): + traverse = Traverse(operation_tree) + assert len(traverse.traverse()) == 3 + +def test_traverse_large_tree(large_operation_tree): + traverse = Traverse(large_operation_tree) + assert len(traverse.traverse()) == 7 + +def test_traverse_type(large_operation_tree): + traverse = Traverse(large_operation_tree) + assert len(traverse.traverse(Addition)) == 3 + assert len(traverse.traverse(Constant)) == 4 + +def test_traverse_loop(operation_tree): + add_oper_signal_source = SignalSource(operation_tree, 0) + add_oper_signal_dest = SignalDestination(operation_tree, 0) + add_oper_signal = Signal(add_oper_signal_source, add_oper_signal_dest) + operation_tree._output_ports[0].connect(add_oper_signal) + operation_tree._input_ports[0].connect(add_oper_signal) + traverse = Traverse(operation_tree) + assert len(traverse.traverse()) == 2 \ No newline at end of file -- GitLab From bacb62bd474a6a12e3b8e8daaa897589e7b33d2f Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Tue, 3 Mar 2020 11:06:53 +0100 Subject: [PATCH 23/50] Update .gitlab-ci.yml to use pytest --- .gitlab-ci.yml | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 78499e52..a10b243e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,9 +1,18 @@ +image: python:3.6 + stages: - - build + - test + +before_script: + - pip3 install . + - pip3 show myapp -PythonBuild: - stage: build - artifacts: - untracked: true +run tests: + stage: test + only: + - develop + tags: + - python3 + - test script: - - apt-get update && apt-get install python3 -y \ No newline at end of file + - pytest test \ No newline at end of file -- GitLab From c80b391875e029d5ef97ff312e395cb5f03db3bf Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Tue, 3 Mar 2020 11:15:18 +0100 Subject: [PATCH 24/50] Removed tags from .gitlab-ci.yml --- .gitlab-ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a10b243e..f9766baf 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,8 +11,5 @@ run tests: stage: test only: - develop - tags: - - python3 - - test script: - pytest test \ No newline at end of file -- GitLab From 71f1c87b05f93ecde4c830767fcda50e1028ac21 Mon Sep 17 00:00:00 2001 From: Jacob Wahlman <jacwa448@student.liu.se> Date: Tue, 3 Mar 2020 11:33:03 +0100 Subject: [PATCH 25/50] doxygen docstrings and some pep stuff --- b_asic/__init__.py | 2 +- b_asic/basic_operation.py | 20 +++++------- b_asic/core_operations.py | 35 ++++++++------------ b_asic/graph_id.py | 14 +++----- b_asic/operation.py | 54 ++++++++++--------------------- b_asic/port.py | 64 ++++++++++++------------------------- b_asic/precedence_chart.py | 8 ++--- b_asic/schema.py | 8 ++--- b_asic/signal.py | 22 +++---------- b_asic/signal_flow_graph.py | 29 +++++++---------- b_asic/simulation.py | 16 +++------- b_asic/traverse_tree.py | 15 ++------- 12 files changed, 91 insertions(+), 196 deletions(-) diff --git a/b_asic/__init__.py b/b_asic/__init__.py index 8a84a945..752bac07 100644 --- a/b_asic/__init__.py +++ b/b_asic/__init__.py @@ -11,4 +11,4 @@ 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 * \ No newline at end of file +from b_asic.simulation import * diff --git a/b_asic/basic_operation.py b/b_asic/basic_operation.py index 4f7b426b..54aaaebe 100644 --- a/b_asic/basic_operation.py +++ b/b_asic/basic_operation.py @@ -1,20 +1,20 @@ -""" +"""@package docstring B-ASIC Basic Operation Module. TODO: More info. """ +from abc import abstractmethod +from typing import List, Dict, Optional, Any +from numbers import Number + from b_asic.port import InputPort, OutputPort from b_asic.signal import SignalSource, SignalDestination from b_asic.operation import Operation from b_asic.simulation import SimulationState, OperationState -from abc import ABC, abstractmethod -from typing import List, Dict, Optional, Any -from numbers import Number class BasicOperation(Operation): - """ - Generic abstract operation class which most implementations will derive from. + """Generic abstract operation class which most implementations will derive from. TODO: More info. """ @@ -23,18 +23,14 @@ class BasicOperation(Operation): _parameters: Dict[str, Optional[Any]] def __init__(self): - """ - Construct a BasicOperation. - """ + """Construct a BasicOperation.""" self._input_ports = [] self._output_ports = [] self._parameters = {} @abstractmethod def evaluate(self, inputs: list) -> list: - """ - 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.""" pass def inputs(self) -> List[InputPort]: diff --git a/b_asic/core_operations.py b/b_asic/core_operations.py index bca344cf..513fe7cf 100644 --- a/b_asic/core_operations.py +++ b/b_asic/core_operations.py @@ -1,18 +1,18 @@ -""" +"""@package docstring B-ASIC Core Operations Module. TODO: More info. """ +from numbers import Number + from b_asic.port import InputPort, OutputPort from b_asic.operation import Operation from b_asic.basic_operation import BasicOperation from b_asic.graph_id import GraphIDType -from numbers import Number class Input(Operation): - """ - Input operation. + """Input operation. TODO: More info. """ @@ -21,15 +21,12 @@ class Input(Operation): class Constant(BasicOperation): - """ - Constant value operation. + """Constant value operation. TODO: More info. """ def __init__(self, value: Number): - """ - Construct a Constant. - """ + """Construct a Constant.""" super().__init__() self._output_ports = [OutputPort(1)] # TODO: Generate appropriate ID for ports. self._parameters["value"] = value @@ -37,19 +34,16 @@ class Constant(BasicOperation): def evaluate(self, inputs: list) -> list: return [self.param("value")] - def get_op_name(self) -> GraphIDType: + def type_name(self) -> GraphIDType: return "const" class Addition(BasicOperation): - """ - Binary addition operation. + """Binary addition operation. TODO: More info. """ def __init__(self): - """ - Construct an Addition. - """ + """Construct an Addition.""" super().__init__() self._input_ports = [InputPort(1), InputPort(1)] # TODO: Generate appropriate ID for ports. self._output_ports = [OutputPort(1)] # TODO: Generate appropriate ID for ports. @@ -57,20 +51,17 @@ class Addition(BasicOperation): def evaluate(self, inputs: list) -> list: return [inputs[0] + inputs[1]] - def get_op_name(self) -> GraphIDType: + def type_name(self) -> GraphIDType: return "add" class ConstantMultiplication(BasicOperation): - """ - Unary constant multiplication operation. + """Unary constant multiplication operation. TODO: More info. """ def __init__(self, coefficient: Number): - """ - Construct a ConstantMultiplication. - """ + """Construct a ConstantMultiplication.""" super().__init__() self._input_ports = [InputPort(1)] # TODO: Generate appropriate ID for ports. self._output_ports = [OutputPort(1)] # TODO: Generate appropriate ID for ports. @@ -79,7 +70,7 @@ class ConstantMultiplication(BasicOperation): def evaluate(self, inputs: list) -> list: return [inputs[0] * self.param("coefficient")] - def get_op_name(self) -> GraphIDType: + def type_name(self) -> GraphIDType: return "const_mul" # TODO: More operations. diff --git a/b_asic/graph_id.py b/b_asic/graph_id.py index 3f25f513..0fd1855b 100644 --- a/b_asic/graph_id.py +++ b/b_asic/graph_id.py @@ -1,19 +1,18 @@ -""" +"""@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, Union, 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. - """ + """A class that generates Graph IDs for objects.""" _next_id_number: DefaultDict[GraphIDType, GraphIDNumber] @@ -21,10 +20,7 @@ class GraphIDGenerator: self._next_id_number = defaultdict(lambda: 1) # Initalises every key element to 1 def get_next_id(self, graph_id_type: GraphIDType) -> GraphID: - """ - Returns the next graph id for a certain graph id type. - """ + """Returns 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 f02cd700..923690aa 100644 --- a/b_asic/operation.py +++ b/b_asic/operation.py @@ -1,115 +1,95 @@ -""" +"""@package docstring B-ASIC Operation Module. TODO: More info. """ from abc import ABC, abstractmethod from numbers import Number -from typing import NewType, List, Dict, Optional, Any, TYPE_CHECKING +from typing import List, Dict, Optional, Any, TYPE_CHECKING if TYPE_CHECKING: from b_asic.port import InputPort, OutputPort from b_asic.simulation import SimulationState from b_asic.graph_id import GraphIDType + class Operation(ABC): - """ - Operation interface. + """Operation interface. TODO: More info. """ @abstractmethod def inputs(self) -> "List[InputPort]": - """ - Get a list of all input ports. - """ + """Get a list of all input ports.""" pass @abstractmethod def outputs(self) -> "List[OutputPort]": - """ - Get a list of all output ports. - """ + """Get a list of all output ports.""" pass @abstractmethod def input_count(self) -> int: - """ - Get the number of input ports. - """ + """Get the number of input ports.""" pass @abstractmethod def output_count(self) -> int: - """ - Get the number of output ports. - """ + """Get the number of output ports.""" pass @abstractmethod def input(self, i: int) -> "InputPort": - """ - Get the input port at index i. - """ + """Get the input port at index i.""" pass @abstractmethod def output(self, i: int) -> "OutputPort": - """ - Get the output port at index i. - """ + """Get the output port at index i.""" pass @abstractmethod def params(self) -> Dict[str, Optional[Any]]: - """ - Get a dictionary of all parameter values. - """ + """Get a dictionary of all parameter values.""" pass @abstractmethod def param(self, name: str) -> Optional[Any]: - """ - Get the value of a parameter. + """Get the value of a parameter. Returns None if the parameter is not defined. """ pass @abstractmethod def set_param(self, name: str, value: Any) -> None: - """ - Set the value of a parameter. + """Set the value of a parameter. The parameter must be defined. """ pass @abstractmethod def evaluate_outputs(self, state: "SimulationState") -> List[Number]: - """ - Simulate the circuit until its iteration count matches that of the simulation state, + """Simulate the circuit until its iteration count matches that of the simulation state, then return the resulting output vector. """ pass @abstractmethod def split(self) -> "List[Operation]": - """ - Split the operation into multiple operations. + """Split the operation into multiple operations. If splitting is not possible, this may return a list containing only the operation itself. """ pass @abstractmethod - def get_op_name(self) -> "GraphIDType": + def type_name(self) -> "GraphIDType": """Returns a string representing the operation name of the operation.""" pass @abstractmethod def neighbours(self) -> "List[Operation]": - """ - Return all operations that are connected by signals to this operation. + """Return all operations that are connected by signals to this operation. If no neighbours are found this returns an empty list """ # TODO: More stuff. - diff --git a/b_asic/port.py b/b_asic/port.py index 2d5405ed..f67defb7 100644 --- a/b_asic/port.py +++ b/b_asic/port.py @@ -1,120 +1,98 @@ -""" +"""@package docstring B-ASIC Port Module. TODO: More info. """ -from b_asic.signal import Signal from abc import ABC, abstractmethod -from typing import NewType, Optional, List, Dict +from typing import NewType, Optional, List + +from b_asic.signal import Signal PortId = NewType("PortId", int) class Port(ABC): - """ - Abstract port class. + """Abstract port class. TODO: More info. """ _identifier: PortId def __init__(self, identifier: PortId): - """ - Construct a Port. - """ + """Construct a Port.""" self._identifier = identifier def identifier(self) -> PortId: - """ - Get the unique identifier. - """ + """Get the unique identifier.""" return self._identifier @abstractmethod def signals(self) -> List[Signal]: - """ - Get a list of all connected signals. - """ + """Get a list of all connected signals.""" pass @abstractmethod def signal_count(self) -> int: - """ - Get the number of connected signals. - """ + """Get the number of connected signals.""" pass @abstractmethod def signal(self, i: int = 0) -> Signal: - """ - Get the connected signal at index i. - """ + """Get the connected signal at index i.""" pass @abstractmethod def connect(self, signal: Signal) -> None: - """ - Connect a signal. - """ + """Connect a signal.""" pass @abstractmethod def disconnect(self, i: int = 0) -> None: - """ - Disconnect a signal. - """ + """Disconnect a signal.""" pass # TODO: More stuff. class InputPort(Port): - """ - Input port. + """Input port. TODO: More info. """ _source_signal: Optional[Signal] def __init__(self, identifier: PortId): - """ - Construct an InputPort. - """ super().__init__(identifier) self._source_signal = None def signals(self) -> List[Signal]: - return [] if self._source_signal == None else [self._source_signal] + return [] if self._source_signal is None else [self._source_signal] def signal_count(self) -> int: - return 0 if self._source_signal == None else 1 + return 0 if self._source_signal is None else 1 def signal(self, i: int = 0) -> Signal: - assert i >= 0 and i < self.signal_count() # TODO: Error message. - assert self._source_signal != None # TODO: Error message. + assert 0 <= i < self.signal_count() # TODO: Error message. + assert self._source_signal is not None # TODO: Error message. return self._source_signal def connect(self, signal: Signal) -> None: self._source_signal = signal def disconnect(self, i: int = 0) -> None: - assert i >= 0 and i < self.signal_count() # TODO: Error message. + assert 0 <= i < self.signal_count() # TODO: Error message. self._source_signal = None # TODO: More stuff. class OutputPort(Port): - """ - Output port. + """Output port. TODO: More info. """ _destination_signals: List[Signal] def __init__(self, identifier: PortId): - """ - Construct an OutputPort. - """ super().__init__(identifier) self._destination_signals = [] @@ -125,7 +103,7 @@ class OutputPort(Port): return len(self._destination_signals) def signal(self, i: int = 0) -> Signal: - assert i >= 0 and i < self.signal_count() # TODO: Error message. + assert 0 <= i < self.signal_count() # TODO: Error message. return self._destination_signals[i] def connect(self, signal: Signal) -> None: @@ -133,7 +111,7 @@ class OutputPort(Port): self._destination_signals.append(signal) def disconnect(self, i: int = 0) -> None: - assert i >= 0 and i < self.signal_count() # TODO: Error message. + assert 0 <= i < self.signal_count() # TODO: Error message. del self._destination_signals[i] # TODO: More stuff. diff --git a/b_asic/precedence_chart.py b/b_asic/precedence_chart.py index 329c78d2..93b86164 100644 --- a/b_asic/precedence_chart.py +++ b/b_asic/precedence_chart.py @@ -1,4 +1,4 @@ -""" +"""@package docstring B-ASIC Precedence Chart Module. TODO: More info. """ @@ -7,8 +7,7 @@ from b_asic.signal_flow_graph import SFG class PrecedenceChart: - """ - Precedence chart constructed from a signal flow graph. + """Precedence chart constructed from a signal flow graph. TODO: More info. """ @@ -16,9 +15,6 @@ class PrecedenceChart: # TODO: More members. def __init__(self, sfg: SFG): - """ - Construct a PrecedenceChart. - """ self.sfg = sfg # TODO: Implement. diff --git a/b_asic/schema.py b/b_asic/schema.py index 56eb47ff..41938263 100644 --- a/b_asic/schema.py +++ b/b_asic/schema.py @@ -1,4 +1,4 @@ -""" +"""@package docstring B-ASIC Schema Module. TODO: More info. """ @@ -7,8 +7,7 @@ from b_asic.precedence_chart import PrecedenceChart class Schema: - """ - Schema constructed from a precedence chart. + """Schema constructed from a precedence chart. TODO: More info. """ @@ -16,9 +15,6 @@ class Schema: # TODO: More members. def __init__(self, pc: PrecedenceChart): - """ - Construct a Schema. - """ self.pc = pc # TODO: Implement. diff --git a/b_asic/signal.py b/b_asic/signal.py index 6ef55c8d..7e63ebfb 100644 --- a/b_asic/signal.py +++ b/b_asic/signal.py @@ -1,23 +1,19 @@ -""" +"""@package docstring B-ASIC Signal Module. TODO: More info. """ from b_asic.operation import Operation -from typing import NewType + class SignalSource: - """ - Handle to a signal source. + """Handle to a signal source. TODO: More info. """ operation: Operation port_index: int def __init__(self, operation: Operation, port_index: int): - """ - Construct a SignalSource. - """ self.operation = operation self.port_index = port_index @@ -25,17 +21,13 @@ class SignalSource: class SignalDestination: - """ - Handle to a signal destination. + """Handle to a signal destination. TODO: More info. """ operation: Operation port_index: int def __init__(self, operation: Operation, port_index: int): - """ - Construct a SignalDestination. - """ self.operation = operation self.port_index = port_index @@ -43,17 +35,13 @@ class SignalDestination: class Signal: - """ - A connection between two operations consisting of a source and destination handle. + """A connection between two operations consisting of a source and destination handle. TODO: More info. """ source: SignalSource destination: SignalDestination def __init__(self, source: SignalSource, destination: SignalDestination): - """ - Construct a Signal. - """ self.source = source self.destination = destination diff --git a/b_asic/signal_flow_graph.py b/b_asic/signal_flow_graph.py index 9d31b04b..914cf390 100644 --- a/b_asic/signal_flow_graph.py +++ b/b_asic/signal_flow_graph.py @@ -1,19 +1,18 @@ -""" +"""@package docstring B-ASIC Signal Flow Graph Module. TODO: More info. """ +from typing import List, Dict, Union, Optional + from b_asic.operation import Operation from b_asic.basic_operation import BasicOperation from b_asic.signal import Signal, SignalSource, SignalDestination -from b_asic.simulation import SimulationState, OperationState from b_asic.graph_id import GraphIDGenerator, GraphID -from typing import List, Dict, Union, Optional class SFG(BasicOperation): - """ - Signal flow graph. + """Signal flow graph. TODO: More info. """ @@ -21,12 +20,11 @@ class SFG(BasicOperation): _graph_id_generator: GraphIDGenerator def __init__(self, input_destinations: List[SignalDestination], output_sources: List[SignalSource]): - """Constructs an SFG.""" super().__init__() # TODO: Allocate input/output ports with appropriate IDs. - + self._graph_objects_by_id = dict # Map Operation ID to Operation objects - self._graph_id_generator = GraphIDGenerator() + self._graph_id_generator = GraphIDGenerator() # TODO: Traverse the graph between the inputs/outputs and add to self._operations. # TODO: Connect ports with signals with appropriate IDs. @@ -37,23 +35,21 @@ class SFG(BasicOperation): def add_operation(self, operation: Operation) -> GraphID: """Adds the entered operation to the SFG's dictionary of graph objects and returns a generated GraphID for it. - + Keyword arguments: operation: Operation to add to the graph. """ - return self._add_graph_obj(operation, operation.get_op_name()) - + return self._add_graph_obj(operation, operation.type_name()) def add_signal(self, signal: Signal) -> GraphID: """Adds the entered signal to the SFG's dictionary of graph objects and returns a generated GraphID for it. - + Keyword argumentst: signal: Signal to add to the graph. """ return self._add_graph_obj(signal, 'sig') - def find_by_id(self, graph_id: GraphID) -> Optional[Operation]: """Finds a graph object based on the entered Graph ID and returns it. If no graph object with the entered ID was found then returns None. @@ -63,14 +59,11 @@ class SFG(BasicOperation): """ if graph_id in self._graph_objects_by_id: return self._graph_objects_by_id[graph_id] - else: - return None - + return None def _add_graph_obj(self, obj: Union[Operation, Signal], operation_id_type: str): graph_id = self._graph_id_generator.get_next_id(operation_id_type) self._graph_objects_by_id[graph_id] = obj - return graph_id - + return graph_id diff --git a/b_asic/simulation.py b/b_asic/simulation.py index d3d3aaf6..c4f7f8f3 100644 --- a/b_asic/simulation.py +++ b/b_asic/simulation.py @@ -1,15 +1,14 @@ -""" +"""@package docstring B-ASIC Simulation Module. TODO: More info. """ from numbers import Number -from typing import List, Dict +from typing import List class OperationState: - """ - Simulation state of an operation. + """Simulation state of an operation. TODO: More info. """ @@ -17,16 +16,12 @@ class OperationState: iteration: int def __init__(self): - """ - Construct an OperationState. - """ self.output_values = [] self.iteration = 0 class SimulationState: - """ - Simulation state. + """Simulation state. TODO: More info. """ @@ -34,9 +29,6 @@ class SimulationState: iteration: int def __init__(self): - """ - Construct a SimulationState. - """ self.operation_states = {} self.iteration = 0 diff --git a/b_asic/traverse_tree.py b/b_asic/traverse_tree.py index 024a542d..dc00371e 100644 --- a/b_asic/traverse_tree.py +++ b/b_asic/traverse_tree.py @@ -1,11 +1,5 @@ -""" +"""@package docstring B-ASIC Operation Tree Traversing Module. -TODO: - - Get a first operation or? an entire operation tree - - For each start point, follow it to the next operation from it's out port. - - If we are searching for a specific operation end. - - If we are searching for a specific type of operation add the operation to a list and continue. - - When we no more out ports can be traversed return results and end. """ from typing import List, Optional @@ -15,12 +9,7 @@ from b_asic.operation import Operation class Traverse: - """Traverse operation tree. - TODO: - - More info. - - Check if a datastructure other than list suits better as return value. - - Implement the type check for operation. - """ + """Traverse operation tree.""" def __init__(self, operation: Operation): """Construct a TraverseTree.""" -- GitLab From 5d95d67b2f3e33a852fc9b05c26fd8488e304f54 Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Tue, 3 Mar 2020 11:37:03 +0100 Subject: [PATCH 26/50] Update .gitlab-ci.yml --- .gitlab-ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f9766baf..bbed8d34 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,8 +4,11 @@ stages: - test before_script: - - pip3 install . - - pip3 show myapp + - apt-get update --yes + - apt-cache search fmt +# - apt-get install --yes cmake pybind11 +# - pip3 install . +# - pip3 show b_asic run tests: stage: test -- GitLab From 78b73849d135a37e4b7d7ca780a049856fbf6196 Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Tue, 3 Mar 2020 11:46:23 +0100 Subject: [PATCH 27/50] Update .gitlab-ci.yml --- .gitlab-ci.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bbed8d34..daa60558 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,10 +5,9 @@ stages: before_script: - apt-get update --yes - - apt-cache search fmt -# - apt-get install --yes cmake pybind11 -# - pip3 install . -# - pip3 show b_asic + - apt-get install --yes build-essential cmake libfmt-dev pybind11-dev + - pip3 install . + - pip3 show b_asic run tests: stage: test -- GitLab From 2a81edff903f27315358393151e7faf495762942 Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Tue, 3 Mar 2020 12:04:23 +0100 Subject: [PATCH 28/50] Update .gitlab-ci.yml --- .gitlab-ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index daa60558..e5cebcda 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,7 +5,9 @@ stages: before_script: - apt-get update --yes - - apt-get install --yes build-essential cmake libfmt-dev pybind11-dev + - apt-get install --yes build-essential cmake pybind11-dev + - wget http://launchpadlibrarian.net/466750967/libfmt-dev_6.1.2+ds-2_amd64.deb + - apt install http://launchpadlibrarian.net/466750967/libfmt-dev_6.1.2+ds-2_amd64.deb - pip3 install . - pip3 show b_asic -- GitLab From 65d26595458953937f780510eb7b51eba27f5309 Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Tue, 3 Mar 2020 12:05:53 +0100 Subject: [PATCH 29/50] Update .gitlab-ci.yml --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e5cebcda..46fc191c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,7 +7,7 @@ before_script: - apt-get update --yes - apt-get install --yes build-essential cmake pybind11-dev - wget http://launchpadlibrarian.net/466750967/libfmt-dev_6.1.2+ds-2_amd64.deb - - apt install http://launchpadlibrarian.net/466750967/libfmt-dev_6.1.2+ds-2_amd64.deb + - apt install libfmt-dev_6.1.2+ds-2_amd64.deb - pip3 install . - pip3 show b_asic -- GitLab From 8d9cc29a710453723c65640830319b372d35187c Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Tue, 3 Mar 2020 12:07:24 +0100 Subject: [PATCH 30/50] Update .gitlab-ci.yml --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 46fc191c..fdec79f5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,7 +7,7 @@ before_script: - apt-get update --yes - apt-get install --yes build-essential cmake pybind11-dev - wget http://launchpadlibrarian.net/466750967/libfmt-dev_6.1.2+ds-2_amd64.deb - - apt install libfmt-dev_6.1.2+ds-2_amd64.deb + - apt install ./libfmt-dev_6.1.2+ds-2_amd64.deb - pip3 install . - pip3 show b_asic -- GitLab From 9869138862bce68c184957ae4fa775e904613e3f Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Tue, 3 Mar 2020 12:11:57 +0100 Subject: [PATCH 31/50] Update .gitlab-ci.yml --- .gitlab-ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fdec79f5..daa60558 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,9 +5,7 @@ stages: before_script: - apt-get update --yes - - apt-get install --yes build-essential cmake pybind11-dev - - wget http://launchpadlibrarian.net/466750967/libfmt-dev_6.1.2+ds-2_amd64.deb - - apt install ./libfmt-dev_6.1.2+ds-2_amd64.deb + - apt-get install --yes build-essential cmake libfmt-dev pybind11-dev - pip3 install . - pip3 show b_asic -- GitLab From 6b6e6b1544029ca284a3153f58b89e06a6a3c4e4 Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Tue, 3 Mar 2020 12:12:38 +0100 Subject: [PATCH 32/50] Update CMakeLists.txt --- CMakeLists.txt | 156 ++++++++++++++++++++++++------------------------- 1 file changed, 78 insertions(+), 78 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bcd7af2e..a5ee5840 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,79 +1,79 @@ -cmake_minimum_required(VERSION 3.8) - -project( - "B-ASIC" - VERSION 0.0.1 - DESCRIPTION "Better ASIC Toolbox for python3" - LANGUAGES C CXX -) - -find_package(fmt 6.1.2 REQUIRED) -find_package(pybind11 CONFIG REQUIRED) - -set(LIBRARY_NAME "b_asic") -set(TARGET_NAME "_${LIBRARY_NAME}") - -if(NOT CMAKE_LIBRARY_OUTPUT_DIRECTORY) - include(GNUInstallDirs) - set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_INSTALL_LIBDIR}") -endif() -set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") -set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_DEBUG "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") -set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") -set(CMAKE_PDB_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") -set(CMAKE_PDB_OUTPUT_DIRECTORY_DEBUG "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") -set(CMAKE_PDB_OUTPUT_DIRECTORY_RELEASE "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") -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}") - -pybind11_add_module( - "${TARGET_NAME}" - "${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp" -) - -target_include_directories( - "${TARGET_NAME}" - PRIVATE - "${CMAKE_CURRENT_SOURCE_DIR}/src" -) - -target_compile_features( - "${TARGET_NAME}" - PRIVATE - cxx_std_17 -) -target_compile_options( - "${TARGET_NAME}" - PRIVATE - $<$<OR:$<CXX_COMPILER_ID:GNU>,$<CXX_COMPILER_ID:Clang>>: - -W -Wall -Wextra -Werror -Wno-psabi - $<$<CONFIG:Debug>:-g> - $<$<NOT:$<CONFIG:Debug>>:-O3> - > - $<$<CXX_COMPILER_ID:MSVC>: - /W3 /WX /permissive- /utf-8 - $<$<CONFIG:Debug>:/Od> - $<$<NOT:$<CONFIG:Debug>>:/Ot> - > -) - -target_link_libraries( - "${TARGET_NAME}" - PRIVATE - fmt::fmt-header-only -) - -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 +cmake_minimum_required(VERSION 3.8) + +project( + "B-ASIC" + VERSION 0.0.1 + DESCRIPTION "Better ASIC Toolbox for python3" + LANGUAGES C CXX +) + +find_package(fmt 5.2.1 REQUIRED) +find_package(pybind11 CONFIG REQUIRED) + +set(LIBRARY_NAME "b_asic") +set(TARGET_NAME "_${LIBRARY_NAME}") + +if(NOT CMAKE_LIBRARY_OUTPUT_DIRECTORY) + include(GNUInstallDirs) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_INSTALL_LIBDIR}") +endif() +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_DEBUG "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") +set(CMAKE_PDB_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") +set(CMAKE_PDB_OUTPUT_DIRECTORY_DEBUG "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") +set(CMAKE_PDB_OUTPUT_DIRECTORY_RELEASE "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") +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}") + +pybind11_add_module( + "${TARGET_NAME}" + "${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp" +) + +target_include_directories( + "${TARGET_NAME}" + PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/src" +) + +target_compile_features( + "${TARGET_NAME}" + PRIVATE + cxx_std_17 +) +target_compile_options( + "${TARGET_NAME}" + PRIVATE + $<$<OR:$<CXX_COMPILER_ID:GNU>,$<CXX_COMPILER_ID:Clang>>: + -W -Wall -Wextra -Werror -Wno-psabi + $<$<CONFIG:Debug>:-g> + $<$<NOT:$<CONFIG:Debug>>:-O3> + > + $<$<CXX_COMPILER_ID:MSVC>: + /W3 /WX /permissive- /utf-8 + $<$<CONFIG:Debug>:/Od> + $<$<NOT:$<CONFIG:Debug>>:/Ot> + > +) + +target_link_libraries( + "${TARGET_NAME}" + PRIVATE + fmt::fmt-header-only +) + +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 -- GitLab From 590bba7e05546f758017656e8cd01ebc08a49ebf Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Tue, 3 Mar 2020 12:13:12 +0100 Subject: [PATCH 33/50] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bb71c4d7..fd98f919 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ How to build and debug the library during development. The following packages are required in order to build the library: * cmake 3.8+ * gcc 7+/clang 7+/msvc 16+ - * fmtlib 6.1.2+ + * fmtlib 5.2.1+ * pybind11 2.3.0+ * python 3.6+ * setuptools -- GitLab From bd27492d72afbca3c0a50a0573d56e9c575de569 Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Tue, 3 Mar 2020 12:18:23 +0100 Subject: [PATCH 34/50] Update CMakeLists.txt --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a5ee5840..713d4b80 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,7 +63,7 @@ target_compile_options( target_link_libraries( "${TARGET_NAME}" PRIVATE - fmt::fmt-header-only + fmt-header-only ) add_custom_target( -- GitLab From 216d817d2561da31ef01a5e450e2ec5993f891c4 Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Tue, 3 Mar 2020 12:22:48 +0100 Subject: [PATCH 35/50] Update CMakeLists.txt --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 713d4b80..3471fb4f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,7 +63,7 @@ target_compile_options( target_link_libraries( "${TARGET_NAME}" PRIVATE - fmt-header-only + fmt::fmt ) add_custom_target( -- GitLab From 957cb6901e4d7a1347045c3ca733e64a5cd2af8e Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Tue, 3 Mar 2020 12:27:58 +0100 Subject: [PATCH 36/50] Update setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index cfe8605f..8dc782c8 100644 --- a/setup.py +++ b/setup.py @@ -61,6 +61,7 @@ setuptools.setup( 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", -- GitLab From d893d74b0c4180cdb8b3381396a11be23c29f897 Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Tue, 3 Mar 2020 12:37:38 +0100 Subject: [PATCH 37/50] Update setup.py --- setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 8dc782c8..43d55d40 100644 --- a/setup.py +++ b/setup.py @@ -71,9 +71,7 @@ setuptools.setup( install_requires = [ "pybind11>=2.3.0", "numpy", - "install_qt_binding", - "pytest==5.3.4", - "pytest-cov==2.8.1" + "install_qt_binding" ], packages = ["b_asic"], ext_modules = [CMakeExtension("b_asic")], -- GitLab From aa4f7bc54db3ef641cb73e40e4edf8155792a0b3 Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Tue, 3 Mar 2020 12:42:48 +0100 Subject: [PATCH 38/50] Update .gitlab-ci.yml --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index daa60558..752a2af2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,6 +6,7 @@ stages: before_script: - apt-get update --yes - apt-get install --yes build-essential cmake libfmt-dev pybind11-dev + - pip3 install pytest pytest-cov - pip3 install . - pip3 show b_asic -- GitLab From 09e8cc0ab15f53451afd8b43ca17dfe4c469ccd6 Mon Sep 17 00:00:00 2001 From: Felix Goding <felgo673@student.liu.se> Date: Wed, 4 Mar 2020 13:50:20 +0100 Subject: [PATCH 39/50] Update .gitlab-ci.yml --- .gitlab-ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 752a2af2..3b2095bb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,7 +12,5 @@ before_script: run tests: stage: test - only: - - develop script: - pytest test \ No newline at end of file -- GitLab From 35dfc3ed3bb5fd64166ee9c54bbd5a7444f25185 Mon Sep 17 00:00:00 2001 From: Kevin Scott <kevsc634@student.liu.se> Date: Wed, 4 Mar 2020 16:18:39 +0100 Subject: [PATCH 40/50] Resolve "Signal Interface" --- b_asic/basic_operation.py | 16 +++++--- b_asic/core_operations.py | 10 ++--- b_asic/port.py | 56 +++++++++++++++++--------- b_asic/signal.py | 62 ++++++++++++----------------- b_asic/signal_flow_graph.py | 6 ++- test/fixtures/__init__.py | 0 test/fixtures/signal.py | 16 ++------ test/port/__init__.py | 0 test/port/test_inputport.py | 9 ++--- test/port/test_outputport.py | 6 +-- test/port/test_port.py | 25 ------------ test/traverse/test_traverse_tree.py | 19 +++------ 12 files changed, 97 insertions(+), 128 deletions(-) delete mode 100644 test/fixtures/__init__.py delete mode 100644 test/port/__init__.py delete mode 100644 test/port/test_port.py diff --git a/b_asic/basic_operation.py b/b_asic/basic_operation.py index 54aaaebe..93a27222 100644 --- a/b_asic/basic_operation.py +++ b/b_asic/basic_operation.py @@ -8,7 +8,7 @@ from typing import List, Dict, Optional, Any from numbers import Number from b_asic.port import InputPort, OutputPort -from b_asic.signal import SignalSource, SignalDestination +from b_asic.signal import Signal from b_asic.operation import Operation from b_asic.simulation import SimulationState, OperationState @@ -73,7 +73,7 @@ class BasicOperation(Operation): while self_state.iteration < state.iteration: input_values: List[Number] = [0] * input_count for i in range(input_count): - source: SignalSource = self._input_ports[i].signal().source + 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) @@ -81,7 +81,7 @@ class BasicOperation(Operation): self_state.iteration += 1 for i in range(output_count): for signal in self._output_ports[i].signals(): - destination: SignalDestination = signal.destination + destination: Signal = signal.destination destination.evaluate_outputs(state) return self_state.output_values @@ -96,9 +96,13 @@ class BasicOperation(Operation): @property def neighbours(self) -> List[Operation]: neighbours: List[Operation] = [] - for port in self._output_ports + self._input_ports: - for signal in port.signals(): - neighbours += [signal.source.operation, signal.destination.operation] + for port in self._input_ports: + for signal in port.signals: + neighbours.append(signal.source.operation) + + for port in self._output_ports: + for signal in port.signals: + neighbours.append(signal.destination.operation) return neighbours diff --git a/b_asic/core_operations.py b/b_asic/core_operations.py index 513fe7cf..45919b8b 100644 --- a/b_asic/core_operations.py +++ b/b_asic/core_operations.py @@ -28,7 +28,7 @@ class Constant(BasicOperation): def __init__(self, value: Number): """Construct a Constant.""" super().__init__() - self._output_ports = [OutputPort(1)] # TODO: Generate appropriate ID for ports. + self._output_ports = [OutputPort(1, self)] # TODO: Generate appropriate ID for ports. self._parameters["value"] = value def evaluate(self, inputs: list) -> list: @@ -45,8 +45,8 @@ class Addition(BasicOperation): def __init__(self): """Construct an Addition.""" super().__init__() - self._input_ports = [InputPort(1), InputPort(1)] # TODO: Generate appropriate ID for ports. - self._output_ports = [OutputPort(1)] # TODO: Generate appropriate ID for ports. + self._input_ports = [InputPort(1, self), InputPort(1, self)] # TODO: Generate appropriate ID for ports. + self._output_ports = [OutputPort(1, self)] # TODO: Generate appropriate ID for ports. def evaluate(self, inputs: list) -> list: return [inputs[0] + inputs[1]] @@ -63,8 +63,8 @@ class ConstantMultiplication(BasicOperation): def __init__(self, coefficient: Number): """Construct a ConstantMultiplication.""" super().__init__() - self._input_ports = [InputPort(1)] # TODO: Generate appropriate ID for ports. - self._output_ports = [OutputPort(1)] # TODO: Generate appropriate ID for ports. + self._input_ports = [InputPort(1), self] # TODO: Generate appropriate ID for ports. + self._output_ports = [OutputPort(1, self)] # TODO: Generate appropriate ID for ports. self._parameters["coefficient"] = coefficient def evaluate(self, inputs: list) -> list: diff --git a/b_asic/port.py b/b_asic/port.py index f67defb7..4c6fb244 100644 --- a/b_asic/port.py +++ b/b_asic/port.py @@ -7,6 +7,7 @@ from abc import ABC, abstractmethod from typing import NewType, Optional, List from b_asic.signal import Signal +from b_asic.operation import Operation PortId = NewType("PortId", int) @@ -16,29 +17,38 @@ class Port(ABC): TODO: More info. """ - _identifier: PortId + _port_id: PortId + _operation: Operation - def __init__(self, identifier: PortId): - """Construct a Port.""" - self._identifier = identifier + def __init__(self, port_id: PortId, operation: Operation): + self._port_id = port_id + self._operation = operation + @property def identifier(self) -> PortId: """Get the unique identifier.""" - return self._identifier + return self._port_id + @property + def operation(self) -> Operation: + """Get the connected operation.""" + return self._operation + + @property @abstractmethod def signals(self) -> List[Signal]: """Get a list of all connected signals.""" pass + @property @abstractmethod - def signal_count(self) -> int: - """Get the number of connected signals.""" + def signal(self, i: int = 0) -> Signal: + """Get the connected signal at index i.""" pass @abstractmethod - def signal(self, i: int = 0) -> Signal: - """Get the connected signal at index i.""" + def signal_count(self) -> int: + """Get the number of connected signals.""" pass @abstractmethod @@ -51,6 +61,7 @@ class Port(ABC): """Disconnect a signal.""" pass + # TODO: More stuff. @@ -60,26 +71,30 @@ class InputPort(Port): """ _source_signal: Optional[Signal] - def __init__(self, identifier: PortId): - super().__init__(identifier) + def __init__(self, port_id: PortId, operation: Operation): + super().__init__(port_id, operation) self._source_signal = None + @property def signals(self) -> List[Signal]: return [] if self._source_signal is None else [self._source_signal] - def signal_count(self) -> int: - return 0 if self._source_signal is None else 1 - + @property def signal(self, i: int = 0) -> Signal: assert 0 <= i < self.signal_count() # TODO: Error message. assert self._source_signal is not None # TODO: Error message. return self._source_signal + def signal_count(self) -> int: + return 0 if self._source_signal is None else 1 + def connect(self, signal: Signal) -> None: self._source_signal = signal + signal.destination = self def disconnect(self, i: int = 0) -> None: assert 0 <= i < self.signal_count() # TODO: Error message. + self._source_signal.disconnect_source() self._source_signal = None # TODO: More stuff. @@ -92,23 +107,26 @@ class OutputPort(Port): _destination_signals: List[Signal] - def __init__(self, identifier: PortId): - super().__init__(identifier) + def __init__(self, port_id: PortId, operation: Operation): + super().__init__(port_id, operation) self._destination_signals = [] + @property def signals(self) -> List[Signal]: return self._destination_signals.copy() - def signal_count(self) -> int: - return len(self._destination_signals) - + @property def signal(self, i: int = 0) -> Signal: assert 0 <= i < self.signal_count() # TODO: Error message. return self._destination_signals[i] + def signal_count(self) -> int: + return len(self._destination_signals) + def connect(self, signal: Signal) -> None: assert signal not in self._destination_signals # TODO: Error message. self._destination_signals.append(signal) + signal.source = self def disconnect(self, i: int = 0) -> None: assert 0 <= i < self.signal_count() # TODO: Error message. diff --git a/b_asic/signal.py b/b_asic/signal.py index 7e63ebfb..17078138 100644 --- a/b_asic/signal.py +++ b/b_asic/signal.py @@ -1,48 +1,38 @@ """@package docstring B-ASIC Signal Module. -TODO: More info. """ +from typing import TYPE_CHECKING, Optional +if TYPE_CHECKING: + from b_asic import OutputPort, InputPort -from b_asic.operation import Operation - - -class SignalSource: - """Handle to a signal source. - TODO: More info. - """ - operation: Operation - port_index: int - - def __init__(self, operation: Operation, port_index: int): - self.operation = operation - self.port_index = port_index - - # TODO: More stuff. +class Signal: + """A connection between two ports.""" + _source: "OutputPort" + _destination: "InputPort" + def __init__(self, src: Optional["OutputPort"] = None, dest: Optional["InputPort"] = None): + self._source = src + self._destination = dest -class SignalDestination: - """Handle to a signal destination. - TODO: More info. - """ - operation: Operation - port_index: int + @property + def source(self) -> "InputPort": + return self._source - def __init__(self, operation: Operation, port_index: int): - self.operation = operation - self.port_index = port_index + @property + def destination(self) -> "OutputPort": + return self._destination - # TODO: More stuff. + @source.setter + def source(self, src: "Outputport") -> None: + self._source = src + @destination.setter + def destination(self, dest: "InputPort") -> None: + self._destination = dest -class Signal: - """A connection between two operations consisting of a source and destination handle. - TODO: More info. - """ - source: SignalSource - destination: SignalDestination + def disconnect_source(self) -> None: + self._source = None - def __init__(self, source: SignalSource, destination: SignalDestination): - self.source = source - self.destination = destination + def disconnect_destination(self) -> None: + self._destination = None - # TODO: More stuff. diff --git a/b_asic/signal_flow_graph.py b/b_asic/signal_flow_graph.py index 914cf390..f7d4be64 100644 --- a/b_asic/signal_flow_graph.py +++ b/b_asic/signal_flow_graph.py @@ -7,7 +7,9 @@ from typing import List, Dict, Union, Optional from b_asic.operation import Operation from b_asic.basic_operation import BasicOperation -from b_asic.signal import Signal, SignalSource, SignalDestination +from b_asic.signal import Signal +from b_asic.simulation import SimulationState, OperationState +from typing import List from b_asic.graph_id import GraphIDGenerator, GraphID @@ -19,7 +21,7 @@ class SFG(BasicOperation): _graph_objects_by_id: Dict[GraphID, Union[Operation, Signal]] _graph_id_generator: GraphIDGenerator - def __init__(self, input_destinations: List[SignalDestination], output_sources: List[SignalSource]): + def __init__(self, input_destinations: List[Signal], output_sources: List[Signal]): super().__init__() # TODO: Allocate input/output ports with appropriate IDs. diff --git a/test/fixtures/__init__.py b/test/fixtures/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/test/fixtures/signal.py b/test/fixtures/signal.py index 64b96f55..9139e93a 100644 --- a/test/fixtures/signal.py +++ b/test/fixtures/signal.py @@ -1,20 +1,10 @@ import pytest -from b_asic import Signal, SignalSource, SignalDestination, Addition +from b_asic import Signal -""" -Use a fixture for initializing objects and pass them as argument to a test function -""" @pytest.fixture def signal(): - source = SignalSource(Addition(), 1) - dest = SignalDestination(Addition(), 2) - return Signal(source, dest) + return Signal() @pytest.fixture def signals(): - ret = [] - for _ in range(0,3): - source = SignalSource(Addition(), 1) - dest = SignalDestination(Addition(), 2) - ret.append(Signal(source, dest)) - return ret \ No newline at end of file + return [Signal() for _ in range(0,3)] diff --git a/test/port/__init__.py b/test/port/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/test/port/test_inputport.py b/test/port/test_inputport.py index d761900a..f0e70cb7 100644 --- a/test/port/test_inputport.py +++ b/test/port/test_inputport.py @@ -2,22 +2,19 @@ B-ASIC test suite for Inputport """ -# import module we are testing from b_asic import InputPort -# import dependencies -from b_asic import Signal, SignalSource, SignalDestination, Addition - import pytest def test_connect_multiple_signals(signals): """ test if only one signal can connect to an input port """ - inp_port = InputPort(0) + inp_port = InputPort(0, None) for s in signals: inp_port.connect(s) assert inp_port.signal_count() == 1 - assert inp_port.signals()[0] == signals[-1] \ No newline at end of file + assert inp_port.signals[0] == signals[-1] + diff --git a/test/port/test_outputport.py b/test/port/test_outputport.py index 5f7b8f49..5c76bb48 100644 --- a/test/port/test_outputport.py +++ b/test/port/test_outputport.py @@ -2,17 +2,17 @@ B-ASIC test suite for InputPort TODO: More info """ -from b_asic import OutputPort, Signal, SignalSource, SignalDestination, Addition +from b_asic import OutputPort import pytest def test_connect_multiple_signals(signals): """ test if multiple signals can connect to an output port """ - outp_port = OutputPort(0) + outp_port = OutputPort(0, None) for s in signals: outp_port.connect(s) assert outp_port.signal_count() == 3 - assert outp_port.signals() == signals \ No newline at end of file + assert outp_port.signals == signals \ No newline at end of file diff --git a/test/port/test_port.py b/test/port/test_port.py deleted file mode 100644 index 7e1fc9b7..00000000 --- a/test/port/test_port.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -B-ASIC test suite for Port interface, place all general test cases for abstract class Port here -""" - -from b_asic import InputPort, OutputPort, Signal, SignalSource, SignalDestination, Addition -import pytest - - -def test_connect_one_signal_to_port(signal): - port = InputPort(0) - port.connect(signal) - assert len(port.signals()) == 1 - assert port.signal() == signal - -def test_change_port_signal(): - source = SignalSource(Addition, 1) - dest = SignalDestination(Addition,2) - signal1 = Signal(source, dest) - signal2 = Signal(source, dest) - - port = InputPort(0) - port.connect(signal1) - assert port.signal() == signal1 - port.connect(signal2) - assert port.signal() == signal2 \ No newline at end of file diff --git a/test/traverse/test_traverse_tree.py b/test/traverse/test_traverse_tree.py index d218a6e7..57e8a67b 100644 --- a/test/traverse/test_traverse_tree.py +++ b/test/traverse/test_traverse_tree.py @@ -5,7 +5,7 @@ TODO: """ from b_asic.core_operations import Constant, Addition -from b_asic.signal import Signal, SignalSource, SignalDestination +from b_asic.signal import Signal from b_asic.port import InputPort, OutputPort from b_asic.traverse_tree import Traverse @@ -17,10 +17,9 @@ def operation(): def create_operation(_type, dest_oper, index, **kwargs): oper = _type(**kwargs) - oper_signal_source = SignalSource(oper, 0) - oper_signal_dest = SignalDestination(dest_oper, index) - oper_signal = Signal(oper_signal_source, oper_signal_dest) + oper_signal = Signal() oper._output_ports[0].connect(oper_signal) + dest_oper._input_ports[index].connect(oper_signal) return oper @@ -45,15 +44,11 @@ def large_operation_tree(): const_oper_4 = create_operation(Constant, add_oper_2, 1, value=5) add_oper_3 = Addition() - add_oper_signal_source = SignalSource(add_oper, 0) - add_oper_signal_dest = SignalDestination(add_oper_3, 0) - add_oper_signal = Signal(add_oper_signal_source, add_oper_signal_dest) + add_oper_signal = Signal(add_oper, add_oper_3) add_oper._output_ports[0].connect(add_oper_signal) add_oper_3._input_ports[0].connect(add_oper_signal) - add_oper_2_signal_source = SignalSource(add_oper_2, 0) - add_oper_2_signal_dest = SignalDestination(add_oper_3, 1) - add_oper_2_signal = Signal(add_oper_2_signal_source, add_oper_2_signal_dest) + add_oper_2_signal = Signal(add_oper_2, add_oper_3) add_oper_2._output_ports[0].connect(add_oper_2_signal) add_oper_3._input_ports[1].connect(add_oper_2_signal) return const_oper @@ -76,9 +71,7 @@ def test_traverse_type(large_operation_tree): assert len(traverse.traverse(Constant)) == 4 def test_traverse_loop(operation_tree): - add_oper_signal_source = SignalSource(operation_tree, 0) - add_oper_signal_dest = SignalDestination(operation_tree, 0) - add_oper_signal = Signal(add_oper_signal_source, add_oper_signal_dest) + add_oper_signal = Signal() operation_tree._output_ports[0].connect(add_oper_signal) operation_tree._input_ports[0].connect(add_oper_signal) traverse = Traverse(operation_tree) -- GitLab From 5fffc69bcaae6b81ba570e2295f40914c24c5c6e Mon Sep 17 00:00:00 2001 From: Angus Lothian <anglo547@student.liu.se> Date: Thu, 5 Mar 2020 10:57:04 +0100 Subject: [PATCH 41/50] Change so that Basic Operation also inherits from abstract graph component and removed name property implementation --- b_asic/__init__.py | 7 +- b_asic/abstract_graph_component.py | 25 ++++++ ...sic_operation.py => abstract_operation.py} | 14 ++-- b_asic/core_operations.py | 42 +++++----- b_asic/graph_component.py | 34 ++++++++ b_asic/operation.py | 41 +++++----- b_asic/port.py | 16 ++-- b_asic/signal.py | 28 +++++-- b_asic/signal_flow_graph.py | 80 ++++++++++++------- .../test_signal_flow_graph.py | 7 ++ 10 files changed, 197 insertions(+), 97 deletions(-) create mode 100644 b_asic/abstract_graph_component.py rename b_asic/{basic_operation.py => abstract_operation.py} (90%) create mode 100644 b_asic/graph_component.py diff --git a/b_asic/__init__.py b/b_asic/__init__.py index 752bac07..4ae6652b 100644 --- a/b_asic/__init__.py +++ b/b_asic/__init__.py @@ -2,9 +2,11 @@ Better ASIC Toolbox. TODO: More info. """ -from _b_asic import * -from b_asic.basic_operation import * +from b_asic.abstract_graph_component import * +from b_asic.abstract_operation import * 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 * @@ -12,3 +14,4 @@ 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.traverse_tree import * diff --git a/b_asic/abstract_graph_component.py b/b_asic/abstract_graph_component.py new file mode 100644 index 00000000..6efb94e3 --- /dev/null +++ b/b_asic/abstract_graph_component.py @@ -0,0 +1,25 @@ +"""@package docstring +B-ASIC module for Graph Components of a signal flow graph. +TODO: More info. +""" + +from b_asic.graph_component import GraphComponent, Name + +class AbstractGraphComponent(GraphComponent): + """Abstract Graph Component class which is a component of a signal flow graph. + + TODO: More info. + """ + + _name: Name + + def __init__(self, name: Name = ""): + self._name = name + + @property + def name(self) -> Name: + return self._name + + @name.setter + def name(self, name: Name) -> None: + self._name = name diff --git a/b_asic/basic_operation.py b/b_asic/abstract_operation.py similarity index 90% rename from b_asic/basic_operation.py rename to b_asic/abstract_operation.py index 93a27222..ab8438a5 100644 --- a/b_asic/basic_operation.py +++ b/b_asic/abstract_operation.py @@ -1,5 +1,5 @@ """@package docstring -B-ASIC Basic Operation Module. +B-ASIC Abstract Operation Module. TODO: More info. """ @@ -11,9 +11,9 @@ from b_asic.port import InputPort, OutputPort from b_asic.signal import Signal from b_asic.operation import Operation from b_asic.simulation import SimulationState, OperationState +from b_asic.abstract_graph_component import AbstractGraphComponent - -class BasicOperation(Operation): +class AbstractOperation(Operation, AbstractGraphComponent): """Generic abstract operation class which most implementations will derive from. TODO: More info. """ @@ -22,8 +22,8 @@ class BasicOperation(Operation): _output_ports: List[OutputPort] _parameters: Dict[str, Optional[Any]] - def __init__(self): - """Construct a BasicOperation.""" + def __init__(self, **kwds): + super().__init__(**kwds) self._input_ports = [] self._output_ports = [] self._parameters = {} @@ -31,7 +31,7 @@ class BasicOperation(Operation): @abstractmethod def evaluate(self, inputs: list) -> list: """Evaluate the operation and generate a list of output values given a list of input values.""" - pass + raise NotImplementedError def inputs(self) -> List[InputPort]: return self._input_ports.copy() @@ -68,7 +68,7 @@ class BasicOperation(Operation): 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.identifier()] + self_state: OperationState = state.operation_states[self] while self_state.iteration < state.iteration: input_values: List[Number] = [0] * input_count diff --git a/b_asic/core_operations.py b/b_asic/core_operations.py index 45919b8b..52f18361 100644 --- a/b_asic/core_operations.py +++ b/b_asic/core_operations.py @@ -7,62 +7,69 @@ from numbers import Number from b_asic.port import InputPort, OutputPort from b_asic.operation import Operation -from b_asic.basic_operation import BasicOperation -from b_asic.graph_id import GraphIDType +from b_asic.abstract_operation import AbstractOperation +from b_asic.abstract_graph_component import AbstractGraphComponent +from b_asic.graph_component import Name, TypeName -class Input(Operation): +class Input(Operation, AbstractGraphComponent): """Input operation. TODO: More info. """ # TODO: Implement all functions. - pass + @property + def type_name(self) -> TypeName: + return "in" -class Constant(BasicOperation): + +class Constant(AbstractOperation): """Constant value operation. TODO: More info. """ - def __init__(self, value: Number): + def __init__(self, value: Number, **kwds): """Construct a Constant.""" - super().__init__() + super().__init__(**kwds) self._output_ports = [OutputPort(1, self)] # TODO: Generate appropriate ID for ports. self._parameters["value"] = value def evaluate(self, inputs: list) -> list: return [self.param("value")] - def type_name(self) -> GraphIDType: + @property + def type_name(self) -> TypeName: return "const" -class Addition(BasicOperation): + +class Addition(AbstractOperation): """Binary addition operation. TODO: More info. """ - def __init__(self): + def __init__(self, **kwds): """Construct an Addition.""" - super().__init__() + super().__init__(**kwds) self._input_ports = [InputPort(1, self), InputPort(1, self)] # TODO: Generate appropriate ID for ports. self._output_ports = [OutputPort(1, self)] # TODO: Generate appropriate ID for ports. def evaluate(self, inputs: list) -> list: return [inputs[0] + inputs[1]] - def type_name(self) -> GraphIDType: + @property + def type_name(self) -> TypeName: return "add" -class ConstantMultiplication(BasicOperation): +class ConstantMultiplication(AbstractOperation): """Unary constant multiplication operation. TODO: More info. """ - def __init__(self, coefficient: Number): + def __init__(self, coefficient: Number, **kwds): """Construct a ConstantMultiplication.""" - super().__init__() + super().__init__(**kwds) self._input_ports = [InputPort(1), self] # TODO: Generate appropriate ID for ports. self._output_ports = [OutputPort(1, self)] # TODO: Generate appropriate ID for ports. self._parameters["coefficient"] = coefficient @@ -70,7 +77,6 @@ class ConstantMultiplication(BasicOperation): def evaluate(self, inputs: list) -> list: return [inputs[0] * self.param("coefficient")] - def type_name(self) -> GraphIDType: + @property + def type_name(self) -> TypeName: return "const_mul" - -# TODO: More operations. diff --git a/b_asic/graph_component.py b/b_asic/graph_component.py new file mode 100644 index 00000000..4bce63f2 --- /dev/null +++ b/b_asic/graph_component.py @@ -0,0 +1,34 @@ +"""@package docstring +B-ASIC Operation Module. +TODO: More info. +""" + +from abc import ABC, abstractmethod +from typing import NewType + +Name = NewType("Name", str) +TypeName = NewType("TypeName", str) + + +class GraphComponent(ABC): + """Graph component interface. + TODO: More info. + """ + + @property + @abstractmethod + def type_name(self) -> TypeName: + """Returns the type name of the graph component""" + raise NotImplementedError + + @property + @abstractmethod + def name(self) -> Name: + """Returns the name of the graph component.""" + raise NotImplementedError + + @name.setter + @abstractmethod + def name(self, name: Name) -> None: + """Sets the name of the graph component to the entered name.""" + raise NotImplementedError diff --git a/b_asic/operation.py b/b_asic/operation.py index 923690aa..5d4b404d 100644 --- a/b_asic/operation.py +++ b/b_asic/operation.py @@ -3,17 +3,17 @@ B-ASIC Operation Module. TODO: More info. """ -from abc import ABC, abstractmethod +from abc import abstractmethod from numbers import Number from typing import List, Dict, Optional, Any, TYPE_CHECKING +from b_asic.graph_component import GraphComponent + if TYPE_CHECKING: from b_asic.port import InputPort, OutputPort from b_asic.simulation import SimulationState - from b_asic.graph_id import GraphIDType - -class Operation(ABC): +class Operation(GraphComponent): """Operation interface. TODO: More info. """ @@ -21,75 +21,72 @@ class Operation(ABC): @abstractmethod def inputs(self) -> "List[InputPort]": """Get a list of all input ports.""" - pass + raise NotImplementedError @abstractmethod def outputs(self) -> "List[OutputPort]": """Get a list of all output ports.""" - pass + raise NotImplementedError @abstractmethod def input_count(self) -> int: """Get the number of input ports.""" - pass + raise NotImplementedError @abstractmethod def output_count(self) -> int: """Get the number of output ports.""" - pass + raise NotImplementedError @abstractmethod def input(self, i: int) -> "InputPort": """Get the input port at index i.""" - pass + raise NotImplementedError + @abstractmethod def output(self, i: int) -> "OutputPort": """Get the output port at index i.""" - pass + raise NotImplementedError + @abstractmethod def params(self) -> Dict[str, Optional[Any]]: """Get a dictionary of all parameter values.""" - pass + raise NotImplementedError @abstractmethod def param(self, name: str) -> Optional[Any]: """Get the value of a parameter. Returns None if the parameter is not defined. """ - pass + raise NotImplementedError @abstractmethod def set_param(self, name: str, value: Any) -> None: """Set the value of a parameter. The parameter must be defined. """ - pass + 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. """ - pass + raise NotImplementedError @abstractmethod def split(self) -> "List[Operation]": """Split the operation into multiple operations. If splitting is not possible, this may return a list containing only the operation itself. """ - pass - - @abstractmethod - def type_name(self) -> "GraphIDType": - """Returns a string representing the operation name of the operation.""" - pass + raise NotImplementedError + @property @abstractmethod def neighbours(self) -> "List[Operation]": """Return all operations that are connected by signals to this operation. If no neighbours are found this returns an empty list """ - - # TODO: More stuff. + raise NotImplementedError diff --git a/b_asic/port.py b/b_asic/port.py index 4c6fb244..a8a062fc 100644 --- a/b_asic/port.py +++ b/b_asic/port.py @@ -38,31 +38,27 @@ class Port(ABC): @abstractmethod def signals(self) -> List[Signal]: """Get a list of all connected signals.""" - pass + raise NotImplementedError - @property @abstractmethod def signal(self, i: int = 0) -> Signal: """Get the connected signal at index i.""" - pass + raise NotImplementedError @abstractmethod def signal_count(self) -> int: """Get the number of connected signals.""" - pass + raise NotImplementedError @abstractmethod def connect(self, signal: Signal) -> None: """Connect a signal.""" - pass + raise NotImplementedError @abstractmethod def disconnect(self, i: int = 0) -> None: """Disconnect a signal.""" - pass - - - # TODO: More stuff. + raise NotImplementedError class InputPort(Port): @@ -79,7 +75,6 @@ class InputPort(Port): def signals(self) -> List[Signal]: return [] if self._source_signal is None else [self._source_signal] - @property def signal(self, i: int = 0) -> Signal: assert 0 <= i < self.signal_count() # TODO: Error message. assert self._source_signal is not None # TODO: Error message. @@ -115,7 +110,6 @@ class OutputPort(Port): def signals(self) -> List[Signal]: return self._destination_signals.copy() - @property def signal(self, i: int = 0) -> Signal: assert 0 <= i < self.signal_count() # TODO: Error message. return self._destination_signals[i] diff --git a/b_asic/signal.py b/b_asic/signal.py index 17078138..810c00dc 100644 --- a/b_asic/signal.py +++ b/b_asic/signal.py @@ -2,37 +2,51 @@ B-ASIC Signal Module. """ from typing import TYPE_CHECKING, Optional + +from b_asic.graph_component import TypeName +from b_asic.abstract_graph_component import AbstractGraphComponent + if TYPE_CHECKING: from b_asic import OutputPort, InputPort -class Signal: +class Signal(AbstractGraphComponent): """A connection between two ports.""" _source: "OutputPort" _destination: "InputPort" - def __init__(self, src: Optional["OutputPort"] = None, dest: Optional["InputPort"] = None): + def __init__(self, src: Optional["OutputPort"] = None, dest: Optional["InputPort"] = None, **kwds): + super().__init__(**kwds) self._source = src self._destination = dest - @property - def source(self) -> "InputPort": + @property + def source(self) -> "OutputPort": + """Returns the source OutputPort of the signal.""" return self._source @property - def destination(self) -> "OutputPort": + def destination(self) -> "InputPort": + """Returns the destination InputPort of the signal.""" return self._destination @source.setter - def source(self, src: "Outputport") -> None: + def source(self, src: "OutputPort") -> None: + """Sets the value of the source OutputPort of the signal.""" self._source = src @destination.setter def destination(self, dest: "InputPort") -> None: + """Sets the value of the destination InputPort of the signal.""" self._destination = dest + @property + def type_name(self) -> TypeName: + return "s" + def disconnect_source(self) -> None: + """Disconnects the source OutputPort of the signal.""" self._source = None def disconnect_destination(self) -> None: + """Disconnects the destination InputPort of the signal.""" self._destination = None - diff --git a/b_asic/signal_flow_graph.py b/b_asic/signal_flow_graph.py index f7d4be64..38c46697 100644 --- a/b_asic/signal_flow_graph.py +++ b/b_asic/signal_flow_graph.py @@ -3,69 +3,89 @@ B-ASIC Signal Flow Graph Module. TODO: More info. """ -from typing import List, Dict, Union, Optional +from typing import List, Dict, Optional, DefaultDict +from collections import defaultdict from b_asic.operation import Operation -from b_asic.basic_operation import BasicOperation +from b_asic.abstract_operation import AbstractOperation from b_asic.signal import Signal -from b_asic.simulation import SimulationState, OperationState -from typing import List from b_asic.graph_id import GraphIDGenerator, GraphID +from b_asic.graph_component import GraphComponent, Name, TypeName -class SFG(BasicOperation): +class SFG(AbstractOperation): """Signal flow graph. TODO: More info. """ - _graph_objects_by_id: Dict[GraphID, Union[Operation, Signal]] + _graph_components_by_id: Dict[GraphID, GraphComponent] + _graph_components_by_name: DefaultDict[Name, List[GraphComponent]] _graph_id_generator: GraphIDGenerator - def __init__(self, input_destinations: List[Signal], output_sources: List[Signal]): - super().__init__() - # TODO: Allocate input/output ports with appropriate IDs. - - self._graph_objects_by_id = dict # Map Operation ID to Operation objects + 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 = [] + + 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() + for operation in ops: + self._add_graph_component(operation) + + for input_signal in input_signals: + self._add_graph_component(input_signal) + + # 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. def evaluate(self, inputs: list) -> list: return [] # TODO: Implement - def add_operation(self, operation: Operation) -> GraphID: - """Adds the entered operation to the SFG's dictionary of graph objects and + def _add_graph_component(self, graph_component: GraphComponent) -> GraphID: + """Adds the entered graph component to the SFG's dictionary of graph objects and returns a generated GraphID for it. Keyword arguments: - operation: Operation to add to the graph. + graph_component: Graph component to add to the graph. """ - return self._add_graph_obj(operation, operation.type_name()) + # Add to name dict + self._graph_components_by_name[graph_component.name].append(graph_component) - def add_signal(self, signal: Signal) -> GraphID: - """Adds the entered signal to the SFG's dictionary of graph objects and returns - a generated GraphID for it. - - Keyword argumentst: - signal: Signal to add to the graph. - """ - return self._add_graph_obj(signal, 'sig') + # 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 - def find_by_id(self, graph_id: GraphID) -> Optional[Operation]: + def find_by_id(self, graph_id: GraphID) -> Optional[GraphComponent]: """Finds a graph object based on the entered Graph ID and returns it. If no graph object with the entered ID was found then returns None. Keyword arguments: graph_id: Graph ID of the wanted object. """ - if graph_id in self._graph_objects_by_id: - return self._graph_objects_by_id[graph_id] + if graph_id in self._graph_components_by_id: + return self._graph_components_by_id[graph_id] return None - def _add_graph_obj(self, obj: Union[Operation, Signal], operation_id_type: str): - graph_id = self._graph_id_generator.get_next_id(operation_id_type) - self._graph_objects_by_id[graph_id] = obj - return graph_id + def find_by_name(self, name: Name) -> List[GraphComponent]: + """Finds all graph objects that have the entered name and returns them + in a list. If no graph object with the entered name was found then returns an + empty list. + + Keyword arguments: + name: Name of the wanted object. + """ + return self._graph_components_by_name[name] + @property + def type_name(self) -> TypeName: + return "sfg" diff --git a/test/signal_flow_graph/test_signal_flow_graph.py b/test/signal_flow_graph/test_signal_flow_graph.py index 921e8906..d18d2da5 100644 --- a/test/signal_flow_graph/test_signal_flow_graph.py +++ b/test/signal_flow_graph/test_signal_flow_graph.py @@ -1,3 +1,10 @@ from b_asic.signal_flow_graph import SFG from b_asic.core_operations import Addition, Constant from b_asic.signal import Signal +from b_asic.signal_flow_graph import SFG + +import pytest + +def test_adding_to_sfg(): + pass + -- GitLab From 72cb843b1447226589a05ffa67437d57fc801bcc Mon Sep 17 00:00:00 2001 From: Jacob Wahlman <jacwa448@student.liu.se> Date: Thu, 5 Mar 2020 11:40:09 +0100 Subject: [PATCH 42/50] Resolve "Operation Traversing" --- b_asic/__init__.py | 2 +- b_asic/abstract_operation.py | 7 ++- b_asic/traverse_tree.py | 43 ----------------- b_asic/utilities.py | 21 +++++++++ test/conftest.py | 3 +- test/fixtures/operation_tree.py | 44 +++++++++++++++++ test/traverse/test_traverse_tree.py | 73 +++++------------------------ 7 files changed, 86 insertions(+), 107 deletions(-) delete mode 100644 b_asic/traverse_tree.py create mode 100644 b_asic/utilities.py create mode 100644 test/fixtures/operation_tree.py diff --git a/b_asic/__init__.py b/b_asic/__init__.py index 4ae6652b..fc787edf 100644 --- a/b_asic/__init__.py +++ b/b_asic/__init__.py @@ -14,4 +14,4 @@ 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.traverse_tree import * +from b_asic.utilities import * diff --git a/b_asic/abstract_operation.py b/b_asic/abstract_operation.py index ab8438a5..558022a7 100644 --- a/b_asic/abstract_operation.py +++ b/b_asic/abstract_operation.py @@ -4,13 +4,14 @@ TODO: More info. """ from abc import abstractmethod -from typing import List, Dict, Optional, Any +from typing import List, Set, Dict, Optional, Any from numbers import Number from b_asic.port import InputPort, OutputPort from b_asic.signal import Signal from b_asic.operation import Operation from b_asic.simulation import SimulationState, OperationState +from b_asic.utilities import breadth_first_search from b_asic.abstract_graph_component import AbstractGraphComponent class AbstractOperation(Operation, AbstractGraphComponent): @@ -106,4 +107,8 @@ class AbstractOperation(Operation, AbstractGraphComponent): return neighbours + def traverse(self) -> Operation: + """Traverse the operation tree and return a generator with start point in the operation.""" + return breadth_first_search(self) + # TODO: More stuff. diff --git a/b_asic/traverse_tree.py b/b_asic/traverse_tree.py deleted file mode 100644 index dc00371e..00000000 --- a/b_asic/traverse_tree.py +++ /dev/null @@ -1,43 +0,0 @@ -"""@package docstring -B-ASIC Operation Tree Traversing Module. -""" - -from typing import List, Optional -from collections import deque - -from b_asic.operation import Operation - - -class Traverse: - """Traverse operation tree.""" - - def __init__(self, operation: Operation): - """Construct a TraverseTree.""" - self._initial_operation = operation - - def _breadth_first_search(self, start: Operation) -> List[Operation]: - """Use breadth first search to traverse the operation tree.""" - visited: List[Operation] = [start] - queue = deque([start]) - while queue: - operation = queue.popleft() - for n_operation in operation.neighbours: - if n_operation not in visited: - visited.append(n_operation) - queue.append(n_operation) - - return visited - - def traverse(self, type_: Optional[Operation] = None) -> List[Operation]: - """Traverse the the operation tree and return operation where type matches. - If the type is None then return the entire tree. - - Keyword arguments: - type_-- the operation type to search for (default None) - """ - - operations: List[Operation] = self._breadth_first_search(self._initial_operation) - if type_ is not None: - operations = [oper for oper in operations if isinstance(oper, type_)] - - return operations diff --git a/b_asic/utilities.py b/b_asic/utilities.py new file mode 100644 index 00000000..25707ff8 --- /dev/null +++ b/b_asic/utilities.py @@ -0,0 +1,21 @@ +"""@package docstring +B-ASIC Operation Module. +TODO: More info. +""" + +from typing import Set +from collections import deque + +from b_asic.operation import Operation + +def breadth_first_search(start: Operation) -> Operation: + """Use breadth first search to traverse the operation tree.""" + visited: Set[Operation] = {start} + queue = deque([start]) + while queue: + operation = queue.popleft() + yield operation + for n_operation in operation.neighbours: + if n_operation not in visited: + visited.add(n_operation) + queue.append(n_operation) diff --git a/test/conftest.py b/test/conftest.py index 986af94c..66ee9630 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,2 +1,3 @@ import pytest -from test.fixtures.signal import * \ No newline at end of file +from test.fixtures.signal import * +from test.fixtures.operation_tree import * \ No newline at end of file diff --git a/test/fixtures/operation_tree.py b/test/fixtures/operation_tree.py new file mode 100644 index 00000000..bcc0bb27 --- /dev/null +++ b/test/fixtures/operation_tree.py @@ -0,0 +1,44 @@ +from b_asic.core_operations import Addition, Constant +from b_asic.signal import Signal + +import pytest + +@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].connect(oper_signal) + + dest_oper._input_ports[index].connect(oper_signal) + return oper + +@pytest.fixture +def operation_tree(): + add_oper = Addition() + create_operation(Constant, add_oper, 0, value=2) + create_operation(Constant, add_oper, 1, value=3) + return add_oper + +@pytest.fixture +def large_operation_tree(): + 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) + + create_operation(Constant, add_oper_2, 0, value=4) + create_operation(Constant, add_oper_2, 1, value=5) + + add_oper_3 = Addition() + add_oper_signal = Signal(add_oper, add_oper_3) + add_oper._output_ports[0].connect(add_oper_signal) + add_oper_3._input_ports[0].connect(add_oper_signal) + + add_oper_2_signal = Signal(add_oper_2, add_oper_3) + add_oper_2._output_ports[0].connect(add_oper_2_signal) + add_oper_3._input_ports[1].connect(add_oper_2_signal) + return const_oper diff --git a/test/traverse/test_traverse_tree.py b/test/traverse/test_traverse_tree.py index 57e8a67b..2c1d08fe 100644 --- a/test/traverse/test_traverse_tree.py +++ b/test/traverse/test_traverse_tree.py @@ -1,78 +1,29 @@ -""" -TODO: - - Rewrite to more clean code, not so repetitive - - Update when signals and id's has been merged. -""" - from b_asic.core_operations import Constant, Addition -from b_asic.signal import Signal +from b_asic.signal import Signal from b_asic.port import InputPort, OutputPort -from b_asic.traverse_tree import Traverse import pytest -@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].connect(oper_signal) - - dest_oper._input_ports[index].connect(oper_signal) - return oper - -@pytest.fixture -def operation_tree(): - add_oper = Addition() - - const_oper = create_operation(Constant, add_oper, 0, value=2) - const_oper_2 = create_operation(Constant, add_oper, 1, value=3) - - return add_oper - -@pytest.fixture -def large_operation_tree(): - add_oper = Addition() - add_oper_2 = Addition() - - const_oper = create_operation(Constant, add_oper, 0, value=2) - const_oper_2 = create_operation(Constant, add_oper, 1, value=3) - - const_oper_3 = create_operation(Constant, add_oper_2, 0, value=4) - const_oper_4 = create_operation(Constant, add_oper_2, 1, value=5) - - add_oper_3 = Addition() - add_oper_signal = Signal(add_oper, add_oper_3) - add_oper._output_ports[0].connect(add_oper_signal) - add_oper_3._input_ports[0].connect(add_oper_signal) - - add_oper_2_signal = Signal(add_oper_2, add_oper_3) - add_oper_2._output_ports[0].connect(add_oper_2_signal) - add_oper_3._input_ports[1].connect(add_oper_2_signal) - return const_oper - def test_traverse_single_tree(operation): - traverse = Traverse(operation) - assert traverse.traverse() == [operation] + """Traverse a tree consisting of one operation.""" + constant = Constant(None) + assert list(constant.traverse()) == [constant] def test_traverse_tree(operation_tree): - traverse = Traverse(operation_tree) - assert len(traverse.traverse()) == 3 + """Traverse a basic addition tree with two constants.""" + assert len(list(operation_tree.traverse())) == 3 def test_traverse_large_tree(large_operation_tree): - traverse = Traverse(large_operation_tree) - assert len(traverse.traverse()) == 7 + """Traverse a larger tree.""" + assert len(list(large_operation_tree.traverse())) == 7 def test_traverse_type(large_operation_tree): - traverse = Traverse(large_operation_tree) - assert len(traverse.traverse(Addition)) == 3 - assert len(traverse.traverse(Constant)) == 4 + 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(operation_tree): add_oper_signal = Signal() operation_tree._output_ports[0].connect(add_oper_signal) operation_tree._input_ports[0].connect(add_oper_signal) - traverse = Traverse(operation_tree) - assert len(traverse.traverse()) == 2 \ No newline at end of file + assert len(list(operation_tree.traverse())) == 2 -- GitLab From 75204fba11ef17ee4f322445a980633e4e4d2c5e Mon Sep 17 00:00:00 2001 From: Jacob Wahlman <jacwa448@student.liu.se> Date: Thu, 5 Mar 2020 11:56:35 +0100 Subject: [PATCH 43/50] tabs to spaces --- b_asic/abstract_operation.py | 194 +++++++++++------------ b_asic/core_operations.py | 100 ++++++------ b_asic/operation.py | 156 +++++++++--------- b_asic/port.py | 186 +++++++++++----------- b_asic/precedence_chart.py | 18 +-- b_asic/schema.py | 18 +-- b_asic/signal.py | 82 +++++----- b_asic/signal_flow_graph.py | 150 +++++++++--------- b_asic/simulation.py | 34 ++-- test/graph_id/test_graph_id_generator.py | 1 - test/port/test_outputport.py | 2 +- 11 files changed, 470 insertions(+), 471 deletions(-) diff --git a/b_asic/abstract_operation.py b/b_asic/abstract_operation.py index 558022a7..7cee1790 100644 --- a/b_asic/abstract_operation.py +++ b/b_asic/abstract_operation.py @@ -15,100 +15,100 @@ from b_asic.utilities import breadth_first_search from b_asic.abstract_graph_component import AbstractGraphComponent class AbstractOperation(Operation, AbstractGraphComponent): - """Generic abstract operation class which most implementations will derive from. - TODO: More info. - """ - - _input_ports: List[InputPort] - _output_ports: List[OutputPort] - _parameters: Dict[str, Optional[Any]] - - def __init__(self, **kwds): - super().__init__(**kwds) - self._input_ports = [] - self._output_ports = [] - self._parameters = {} - - @abstractmethod - def evaluate(self, inputs: list) -> list: - """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 outputs(self) -> List[OutputPort]: - return self._output_ports.copy() - - def input_count(self) -> int: - return len(self._input_ports) - - 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) - assert len(self_state.output_values) == output_count # TODO: Error message. - 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] - - @property - def neighbours(self) -> List[Operation]: - neighbours: List[Operation] = [] - for port in self._input_ports: - for signal in port.signals: - neighbours.append(signal.source.operation) - - for port in self._output_ports: - for signal in port.signals: - neighbours.append(signal.destination.operation) - - return neighbours - - def traverse(self) -> Operation: - """Traverse the operation tree and return a generator with start point in the operation.""" - return breadth_first_search(self) - - # TODO: More stuff. + """Generic abstract operation class which most implementations will derive from. + TODO: More info. + """ + + _input_ports: List[InputPort] + _output_ports: List[OutputPort] + _parameters: Dict[str, Optional[Any]] + + def __init__(self, **kwds): + super().__init__(**kwds) + self._input_ports = [] + self._output_ports = [] + self._parameters = {} + + @abstractmethod + def evaluate(self, inputs: list) -> list: + """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 outputs(self) -> List[OutputPort]: + return self._output_ports.copy() + + def input_count(self) -> int: + return len(self._input_ports) + + 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) + assert len(self_state.output_values) == output_count # TODO: Error message. + 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] + + @property + def neighbours(self) -> List[Operation]: + neighbours: List[Operation] = [] + for port in self._input_ports: + for signal in port.signals: + neighbours.append(signal.source.operation) + + for port in self._output_ports: + for signal in port.signals: + neighbours.append(signal.destination.operation) + + return neighbours + + def traverse(self) -> Operation: + """Traverse the operation tree and return a generator with start point in the operation.""" + return breadth_first_search(self) + + # TODO: More stuff. diff --git a/b_asic/core_operations.py b/b_asic/core_operations.py index 52f18361..f023e1a5 100644 --- a/b_asic/core_operations.py +++ b/b_asic/core_operations.py @@ -13,70 +13,70 @@ from b_asic.graph_component import Name, TypeName class Input(Operation, AbstractGraphComponent): - """Input operation. - TODO: More info. - """ + """Input operation. + TODO: More info. + """ - # TODO: Implement all functions. + # TODO: Implement all functions. - @property - def type_name(self) -> TypeName: - return "in" + @property + def type_name(self) -> TypeName: + return "in" class Constant(AbstractOperation): - """Constant value operation. - TODO: More info. - """ + """Constant value operation. + TODO: More info. + """ - def __init__(self, value: Number, **kwds): - """Construct a Constant.""" - super().__init__(**kwds) - self._output_ports = [OutputPort(1, self)] # TODO: Generate appropriate ID for ports. - self._parameters["value"] = value + def __init__(self, value: Number, **kwds): + """Construct a Constant.""" + super().__init__(**kwds) + self._output_ports = [OutputPort(1, self)] # TODO: Generate appropriate ID for ports. + self._parameters["value"] = value - def evaluate(self, inputs: list) -> list: - return [self.param("value")] + def evaluate(self, inputs: list) -> list: + return [self.param("value")] - @property - def type_name(self) -> TypeName: - return "const" + @property + def type_name(self) -> TypeName: + return "const" class Addition(AbstractOperation): - """Binary addition operation. - TODO: More info. - """ + """Binary addition operation. + TODO: More info. + """ - def __init__(self, **kwds): - """Construct an Addition.""" - super().__init__(**kwds) - self._input_ports = [InputPort(1, self), InputPort(1, self)] # TODO: Generate appropriate ID for ports. - self._output_ports = [OutputPort(1, self)] # TODO: Generate appropriate ID for ports. + def __init__(self, **kwds): + """Construct an Addition.""" + super().__init__(**kwds) + self._input_ports = [InputPort(1, self), InputPort(1, self)] # TODO: Generate appropriate ID for ports. + self._output_ports = [OutputPort(1, self)] # TODO: Generate appropriate ID for ports. - def evaluate(self, inputs: list) -> list: - return [inputs[0] + inputs[1]] + def evaluate(self, inputs: list) -> list: + return [inputs[0] + inputs[1]] - @property - def type_name(self) -> TypeName: - return "add" + @property + def type_name(self) -> TypeName: + return "add" class ConstantMultiplication(AbstractOperation): - """Unary constant multiplication operation. - TODO: More info. - """ - - def __init__(self, coefficient: Number, **kwds): - """Construct a ConstantMultiplication.""" - super().__init__(**kwds) - self._input_ports = [InputPort(1), self] # TODO: Generate appropriate ID for ports. - self._output_ports = [OutputPort(1, self)] # TODO: Generate appropriate ID for ports. - self._parameters["coefficient"] = coefficient - - def evaluate(self, inputs: list) -> list: - return [inputs[0] * self.param("coefficient")] - - @property - def type_name(self) -> TypeName: - return "const_mul" + """Unary constant multiplication operation. + TODO: More info. + """ + + def __init__(self, coefficient: Number, **kwds): + """Construct a ConstantMultiplication.""" + super().__init__(**kwds) + self._input_ports = [InputPort(1), self] # TODO: Generate appropriate ID for ports. + self._output_ports = [OutputPort(1, self)] # TODO: Generate appropriate ID for ports. + self._parameters["coefficient"] = coefficient + + def evaluate(self, inputs: list) -> list: + return [inputs[0] * self.param("coefficient")] + + @property + def type_name(self) -> TypeName: + return "const_mul" diff --git a/b_asic/operation.py b/b_asic/operation.py index 5d4b404d..4a716b79 100644 --- a/b_asic/operation.py +++ b/b_asic/operation.py @@ -10,83 +10,83 @@ from typing import List, Dict, Optional, Any, TYPE_CHECKING from b_asic.graph_component import GraphComponent if TYPE_CHECKING: - from b_asic.port import InputPort, OutputPort - from b_asic.simulation import SimulationState + from b_asic.port import InputPort, OutputPort + from b_asic.simulation import SimulationState class Operation(GraphComponent): - """Operation interface. - TODO: More info. - """ - - @abstractmethod - def inputs(self) -> "List[InputPort]": - """Get a list of all input ports.""" - raise NotImplementedError - - @abstractmethod - def outputs(self) -> "List[OutputPort]": - """Get a list of all output ports.""" - raise NotImplementedError - - @abstractmethod - def input_count(self) -> int: - """Get the number of input ports.""" - raise NotImplementedError - - @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.""" - raise NotImplementedError - - - @abstractmethod - def output(self, i: int) -> "OutputPort": - """Get the output port at index i.""" - raise NotImplementedError - - - @abstractmethod - def params(self) -> Dict[str, Optional[Any]]: - """Get a dictionary of all parameter values.""" - raise NotImplementedError - - @abstractmethod - def param(self, name: str) -> Optional[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. - The parameter must be defined. - """ - 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. - """ - raise NotImplementedError - - @abstractmethod - def split(self) -> "List[Operation]": - """Split the operation into multiple operations. - If splitting is not possible, this may return a list containing only the operation itself. - """ - raise NotImplementedError - - @property - @abstractmethod - def neighbours(self) -> "List[Operation]": - """Return all operations that are connected by signals to this operation. - If no neighbours are found this returns an empty list - """ - raise NotImplementedError + """Operation interface. + TODO: More info. + """ + + @abstractmethod + def inputs(self) -> "List[InputPort]": + """Get a list of all input ports.""" + raise NotImplementedError + + @abstractmethod + def outputs(self) -> "List[OutputPort]": + """Get a list of all output ports.""" + raise NotImplementedError + + @abstractmethod + def input_count(self) -> int: + """Get the number of input ports.""" + raise NotImplementedError + + @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.""" + raise NotImplementedError + + + @abstractmethod + def output(self, i: int) -> "OutputPort": + """Get the output port at index i.""" + raise NotImplementedError + + + @abstractmethod + def params(self) -> Dict[str, Optional[Any]]: + """Get a dictionary of all parameter values.""" + raise NotImplementedError + + @abstractmethod + def param(self, name: str) -> Optional[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. + The parameter must be defined. + """ + 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. + """ + raise NotImplementedError + + @abstractmethod + def split(self) -> "List[Operation]": + """Split the operation into multiple operations. + If splitting is not possible, this may return a list containing only the operation itself. + """ + raise NotImplementedError + + @property + @abstractmethod + def neighbours(self) -> "List[Operation]": + """Return all operations that are connected by signals to this operation. + If no neighbours are found this returns an empty list + """ + raise NotImplementedError diff --git a/b_asic/port.py b/b_asic/port.py index a8a062fc..6cbd59ba 100644 --- a/b_asic/port.py +++ b/b_asic/port.py @@ -13,117 +13,117 @@ PortId = NewType("PortId", int) class Port(ABC): - """Abstract port class. - TODO: More info. - """ - - _port_id: PortId - _operation: Operation - - def __init__(self, port_id: PortId, operation: Operation): - self._port_id = port_id - self._operation = operation - - @property - def identifier(self) -> PortId: - """Get the unique identifier.""" - return self._port_id - - @property - def operation(self) -> Operation: - """Get the connected operation.""" - return self._operation - - @property - @abstractmethod - def signals(self) -> List[Signal]: - """Get a list of all connected signals.""" - raise NotImplementedError - - @abstractmethod - def signal(self, i: int = 0) -> Signal: - """Get the connected signal at index i.""" - raise NotImplementedError - - @abstractmethod - def signal_count(self) -> int: - """Get the number of connected signals.""" - raise NotImplementedError - - @abstractmethod - def connect(self, signal: Signal) -> None: - """Connect a signal.""" - raise NotImplementedError - - @abstractmethod - def disconnect(self, i: int = 0) -> None: - """Disconnect a signal.""" - raise NotImplementedError + """Abstract port class. + TODO: More info. + """ + + _port_id: PortId + _operation: Operation + + def __init__(self, port_id: PortId, operation: Operation): + self._port_id = port_id + self._operation = operation + + @property + def identifier(self) -> PortId: + """Get the unique identifier.""" + return self._port_id + + @property + def operation(self) -> Operation: + """Get the connected operation.""" + return self._operation + + @property + @abstractmethod + def signals(self) -> List[Signal]: + """Get a list of all connected signals.""" + raise NotImplementedError + + @abstractmethod + def signal(self, i: int = 0) -> Signal: + """Get the connected signal at index i.""" + raise NotImplementedError + + @abstractmethod + def signal_count(self) -> int: + """Get the number of connected signals.""" + raise NotImplementedError + + @abstractmethod + def connect(self, signal: Signal) -> None: + """Connect a signal.""" + raise NotImplementedError + + @abstractmethod + def disconnect(self, i: int = 0) -> None: + """Disconnect a signal.""" + raise NotImplementedError class InputPort(Port): - """Input port. - TODO: More info. - """ - _source_signal: Optional[Signal] + """Input port. + TODO: More info. + """ + _source_signal: Optional[Signal] - def __init__(self, port_id: PortId, operation: Operation): - super().__init__(port_id, operation) - self._source_signal = None + def __init__(self, port_id: PortId, operation: Operation): + super().__init__(port_id, operation) + self._source_signal = None - @property - def signals(self) -> List[Signal]: - return [] if self._source_signal is None else [self._source_signal] + @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() # TODO: Error message. - assert self._source_signal is not None # TODO: Error message. - return self._source_signal + def signal(self, i: int = 0) -> Signal: + assert 0 <= i < self.signal_count() # TODO: Error message. + assert self._source_signal is not None # TODO: Error message. + return self._source_signal - def signal_count(self) -> int: - return 0 if self._source_signal is None else 1 + def signal_count(self) -> int: + return 0 if self._source_signal is None else 1 - def connect(self, signal: Signal) -> None: - self._source_signal = signal - signal.destination = self + def connect(self, signal: Signal) -> None: + self._source_signal = signal + signal.destination = self - def disconnect(self, i: int = 0) -> None: - assert 0 <= i < self.signal_count() # TODO: Error message. - self._source_signal.disconnect_source() - self._source_signal = None + def disconnect(self, i: int = 0) -> None: + assert 0 <= i < self.signal_count() # TODO: Error message. + self._source_signal.disconnect_source() + self._source_signal = None - # TODO: More stuff. + # TODO: More stuff. class OutputPort(Port): - """Output port. - TODO: More info. - """ + """Output port. + TODO: More info. + """ - _destination_signals: List[Signal] + _destination_signals: List[Signal] - def __init__(self, port_id: PortId, operation: Operation): - super().__init__(port_id, operation) - self._destination_signals = [] + def __init__(self, port_id: PortId, operation: Operation): + super().__init__(port_id, operation) + self._destination_signals = [] - @property - def signals(self) -> List[Signal]: - return self._destination_signals.copy() + @property + def signals(self) -> List[Signal]: + return self._destination_signals.copy() - def signal(self, i: int = 0) -> Signal: - assert 0 <= i < self.signal_count() # TODO: Error message. - return self._destination_signals[i] + def signal(self, i: int = 0) -> Signal: + assert 0 <= i < self.signal_count() # TODO: Error message. + return self._destination_signals[i] - def signal_count(self) -> int: - return len(self._destination_signals) + def signal_count(self) -> int: + return len(self._destination_signals) - def connect(self, signal: Signal) -> None: - assert signal not in self._destination_signals # TODO: Error message. - self._destination_signals.append(signal) - signal.source = self + def connect(self, signal: Signal) -> None: + assert signal not in self._destination_signals # TODO: Error message. + self._destination_signals.append(signal) + signal.source = self - def disconnect(self, i: int = 0) -> None: - assert 0 <= i < self.signal_count() # TODO: Error message. - del self._destination_signals[i] + def disconnect(self, i: int = 0) -> None: + assert 0 <= i < self.signal_count() # TODO: Error message. + del self._destination_signals[i] - # TODO: More stuff. + # TODO: More stuff. diff --git a/b_asic/precedence_chart.py b/b_asic/precedence_chart.py index 93b86164..be55a123 100644 --- a/b_asic/precedence_chart.py +++ b/b_asic/precedence_chart.py @@ -7,15 +7,15 @@ from b_asic.signal_flow_graph import SFG class PrecedenceChart: - """Precedence chart constructed from a signal flow graph. - TODO: More info. - """ + """Precedence chart constructed from a signal flow graph. + TODO: More info. + """ - sfg: SFG - # TODO: More members. + sfg: SFG + # TODO: More members. - def __init__(self, sfg: SFG): - self.sfg = sfg - # TODO: Implement. + def __init__(self, sfg: SFG): + self.sfg = sfg + # TODO: Implement. - # TODO: More stuff. + # TODO: More stuff. diff --git a/b_asic/schema.py b/b_asic/schema.py index 41938263..e5068cdc 100644 --- a/b_asic/schema.py +++ b/b_asic/schema.py @@ -7,15 +7,15 @@ from b_asic.precedence_chart import PrecedenceChart class Schema: - """Schema constructed from a precedence chart. - TODO: More info. - """ + """Schema constructed from a precedence chart. + TODO: More info. + """ - pc: PrecedenceChart - # TODO: More members. + pc: PrecedenceChart + # TODO: More members. - def __init__(self, pc: PrecedenceChart): - self.pc = pc - # TODO: Implement. + def __init__(self, pc: PrecedenceChart): + self.pc = pc + # TODO: Implement. - # TODO: More stuff. + # TODO: More stuff. diff --git a/b_asic/signal.py b/b_asic/signal.py index 810c00dc..4d80530e 100644 --- a/b_asic/signal.py +++ b/b_asic/signal.py @@ -7,46 +7,46 @@ from b_asic.graph_component import TypeName from b_asic.abstract_graph_component import AbstractGraphComponent if TYPE_CHECKING: - from b_asic import OutputPort, InputPort + from b_asic import OutputPort, InputPort class Signal(AbstractGraphComponent): - """A connection between two ports.""" - _source: "OutputPort" - _destination: "InputPort" - - def __init__(self, src: Optional["OutputPort"] = None, dest: Optional["InputPort"] = None, **kwds): - super().__init__(**kwds) - self._source = src - self._destination = dest - - @property - def source(self) -> "OutputPort": - """Returns the source OutputPort of the signal.""" - return self._source - - @property - def destination(self) -> "InputPort": - """Returns the destination InputPort of the signal.""" - return self._destination - - @source.setter - def source(self, src: "OutputPort") -> None: - """Sets the value of the source OutputPort of the signal.""" - self._source = src - - @destination.setter - def destination(self, dest: "InputPort") -> None: - """Sets the value of the destination InputPort of the signal.""" - self._destination = dest - - @property - def type_name(self) -> TypeName: - return "s" - - def disconnect_source(self) -> None: - """Disconnects the source OutputPort of the signal.""" - self._source = None - - def disconnect_destination(self) -> None: - """Disconnects the destination InputPort of the signal.""" - self._destination = None + """A connection between two ports.""" + _source: "OutputPort" + _destination: "InputPort" + + def __init__(self, src: Optional["OutputPort"] = None, dest: Optional["InputPort"] = None, **kwds): + super().__init__(**kwds) + self._source = src + self._destination = dest + + @property + def source(self) -> "OutputPort": + """Returns the source OutputPort of the signal.""" + return self._source + + @property + def destination(self) -> "InputPort": + """Returns the destination InputPort of the signal.""" + return self._destination + + @source.setter + def source(self, src: "OutputPort") -> None: + """Sets the value of the source OutputPort of the signal.""" + self._source = src + + @destination.setter + def destination(self, dest: "InputPort") -> None: + """Sets the value of the destination InputPort of the signal.""" + self._destination = dest + + @property + def type_name(self) -> TypeName: + return "s" + + def disconnect_source(self) -> None: + """Disconnects the source OutputPort of the signal.""" + self._source = None + + def disconnect_destination(self) -> None: + """Disconnects the destination InputPort of the signal.""" + self._destination = None diff --git a/b_asic/signal_flow_graph.py b/b_asic/signal_flow_graph.py index 38c46697..6a91521a 100644 --- a/b_asic/signal_flow_graph.py +++ b/b_asic/signal_flow_graph.py @@ -14,78 +14,78 @@ from b_asic.graph_component import GraphComponent, Name, TypeName class SFG(AbstractOperation): - """Signal flow graph. - TODO: More info. - """ - - _graph_components_by_id: Dict[GraphID, GraphComponent] - _graph_components_by_name: DefaultDict[Name, List[GraphComponent]] - _graph_id_generator: GraphIDGenerator - - 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 = [] - - 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() - - for operation in ops: - self._add_graph_component(operation) - - for input_signal in input_signals: - self._add_graph_component(input_signal) - - # 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. - - def evaluate(self, inputs: list) -> list: - return [] # TODO: Implement - - def _add_graph_component(self, graph_component: GraphComponent) -> GraphID: - """Adds the entered graph component to the SFG's dictionary of graph objects and - returns a generated GraphID for it. - - Keyword arguments: - graph_component: Graph component to add to the graph. - """ - # Add to name dict - self._graph_components_by_name[graph_component.name].append(graph_component) - - # 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 - - def find_by_id(self, graph_id: GraphID) -> Optional[GraphComponent]: - """Finds a graph object based on the entered Graph ID and returns it. If no graph - object with the entered ID was found then returns None. - - Keyword arguments: - graph_id: Graph ID of the wanted object. - """ - if graph_id in self._graph_components_by_id: - return self._graph_components_by_id[graph_id] - - return None - - def find_by_name(self, name: Name) -> List[GraphComponent]: - """Finds all graph objects that have the entered name and returns them - in a list. If no graph object with the entered name was found then returns an - empty list. - - Keyword arguments: - name: Name of the wanted object. - """ - return self._graph_components_by_name[name] - - @property - def type_name(self) -> TypeName: - return "sfg" + """Signal flow graph. + TODO: More info. + """ + + _graph_components_by_id: Dict[GraphID, GraphComponent] + _graph_components_by_name: DefaultDict[Name, List[GraphComponent]] + _graph_id_generator: GraphIDGenerator + + 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 = [] + + 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() + + for operation in ops: + self._add_graph_component(operation) + + for input_signal in input_signals: + self._add_graph_component(input_signal) + + # 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. + + def evaluate(self, inputs: list) -> list: + return [] # TODO: Implement + + def _add_graph_component(self, graph_component: GraphComponent) -> GraphID: + """Adds the entered graph component to the SFG's dictionary of graph objects and + returns a generated GraphID for it. + + Keyword arguments: + graph_component: Graph component to add to the graph. + """ + # Add to name dict + self._graph_components_by_name[graph_component.name].append(graph_component) + + # 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 + + def find_by_id(self, graph_id: GraphID) -> Optional[GraphComponent]: + """Finds a graph object based on the entered Graph ID and returns it. If no graph + object with the entered ID was found then returns None. + + Keyword arguments: + graph_id: Graph ID of the wanted object. + """ + if graph_id in self._graph_components_by_id: + return self._graph_components_by_id[graph_id] + + return None + + def find_by_name(self, name: Name) -> List[GraphComponent]: + """Finds all graph objects that have the entered name and returns them + in a list. If no graph object with the entered name was found then returns an + empty list. + + Keyword arguments: + name: Name of the wanted object. + """ + return self._graph_components_by_name[name] + + @property + def type_name(self) -> TypeName: + return "sfg" diff --git a/b_asic/simulation.py b/b_asic/simulation.py index c4f7f8f3..50adaa52 100644 --- a/b_asic/simulation.py +++ b/b_asic/simulation.py @@ -8,28 +8,28 @@ from typing import List class OperationState: - """Simulation state of an operation. - TODO: More info. - """ + """Simulation state of an operation. + TODO: More info. + """ - output_values: List[Number] - iteration: int + output_values: List[Number] + iteration: int - def __init__(self): - self.output_values = [] - self.iteration = 0 + def __init__(self): + self.output_values = [] + self.iteration = 0 class SimulationState: - """Simulation state. - TODO: More info. - """ + """Simulation state. + TODO: More info. + """ - # operation_states: Dict[OperationId, OperationState] - iteration: int + # operation_states: Dict[OperationId, OperationState] + iteration: int - def __init__(self): - self.operation_states = {} - self.iteration = 0 + def __init__(self): + self.operation_states = {} + self.iteration = 0 - # TODO: More stuff. + # TODO: More stuff. diff --git a/test/graph_id/test_graph_id_generator.py b/test/graph_id/test_graph_id_generator.py index 7aeb6cad..8af36e8c 100644 --- a/test/graph_id/test_graph_id_generator.py +++ b/test/graph_id/test_graph_id_generator.py @@ -26,4 +26,3 @@ def test_different_strings_generator(): 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" - \ No newline at end of file diff --git a/test/port/test_outputport.py b/test/port/test_outputport.py index 5c76bb48..6ca126d5 100644 --- a/test/port/test_outputport.py +++ b/test/port/test_outputport.py @@ -15,4 +15,4 @@ def test_connect_multiple_signals(signals): outp_port.connect(s) assert outp_port.signal_count() == 3 - assert outp_port.signals == signals \ No newline at end of file + assert outp_port.signals == signals -- GitLab From 3d178c98e99d871c21be810e3d661af37d6a31eb Mon Sep 17 00:00:00 2001 From: Kevin Scott <kevsc634@student.liu.se> Date: Fri, 6 Mar 2020 11:48:42 +0100 Subject: [PATCH 44/50] Added comments for fixtures --- test/fixtures/operation_tree.py | 16 ++++++++++++++++ test/fixtures/signal.py | 2 ++ test/graph_id/test_graph_id_generator.py | 13 +++++++------ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/test/fixtures/operation_tree.py b/test/fixtures/operation_tree.py index bcc0bb27..7cc9e422 100644 --- a/test/fixtures/operation_tree.py +++ b/test/fixtures/operation_tree.py @@ -17,6 +17,12 @@ def create_operation(_type, dest_oper, index, **kwargs): @pytest.fixture def operation_tree(): + """ + Return a addition operation connected with 2 constants. + >---C---+ + ---A + >---C---+ + """ add_oper = Addition() create_operation(Constant, add_oper, 0, value=2) create_operation(Constant, add_oper, 1, value=3) @@ -24,6 +30,16 @@ def operation_tree(): @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---+ + """ add_oper = Addition() add_oper_2 = Addition() diff --git a/test/fixtures/signal.py b/test/fixtures/signal.py index 9139e93a..7b13c978 100644 --- a/test/fixtures/signal.py +++ b/test/fixtures/signal.py @@ -3,8 +3,10 @@ from b_asic import Signal @pytest.fixture def signal(): + """Return a signal with no connections.""" return Signal() @pytest.fixture def signals(): + """Return 3 signals with no connections.""" return [Signal() for _ in range(0,3)] diff --git a/test/graph_id/test_graph_id_generator.py b/test/graph_id/test_graph_id_generator.py index 8af36e8c..85fb088d 100644 --- a/test/graph_id/test_graph_id_generator.py +++ b/test/graph_id/test_graph_id_generator.py @@ -6,22 +6,23 @@ from b_asic.graph_id import GraphIDGenerator, GraphID import pytest -def test_empty_string_generator(): +@pytest.fixture +def graph_id_generator(): + return GraphIDGenerator() + +def test_empty_string_generator(graph_id_generator): """Test the graph id generator for an empty string type.""" - graph_id_generator = GraphIDGenerator() assert graph_id_generator.get_next_id("") == "1" assert graph_id_generator.get_next_id("") == "2" -def test_normal_string_generator(): +def test_normal_string_generator(graph_id_generator): """"Test the graph id generator for a normal string type.""" - graph_id_generator = GraphIDGenerator() assert graph_id_generator.get_next_id("add") == "add1" assert graph_id_generator.get_next_id("add") == "add2" -def test_different_strings_generator(): +def test_different_strings_generator(graph_id_generator): """Test the graph id generator for different strings.""" - graph_id_generator = GraphIDGenerator() 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" -- GitLab From c4b4fdaeba80f1f15ff80bf76adbb8ef00861d89 Mon Sep 17 00:00:00 2001 From: Angus Lothian <anglo547@student.liu.se> Date: Thu, 12 Mar 2020 12:12:08 +0100 Subject: [PATCH 45/50] Add updated port and signal interface where connecting / disconnecting is done reflectibly between both signal and port, also add some more helper methods. --- b_asic/abstract_graph_component.py | 1 + b_asic/abstract_operation.py | 11 +- b_asic/core_operations.py | 35 ++++--- b_asic/graph_component.py | 6 +- b_asic/graph_id.py | 2 +- b_asic/operation.py | 2 - b_asic/port.py | 156 ++++++++++++++++++++-------- b_asic/signal.py | 81 +++++++++++---- b_asic/signal_flow_graph.py | 12 +-- test/fixtures/operation_tree.py | 16 +-- test/graph_id/conftest.py | 1 - test/port/test_inputport.py | 84 +++++++++++++-- test/port/test_outputport.py | 35 +++++-- test/signal/test_signal.py | 62 +++++++++++ test/signal_flow_graph/conftest.py | 1 - test/traverse/test_traverse_tree.py | 5 +- 16 files changed, 385 insertions(+), 125 deletions(-) delete mode 100644 test/graph_id/conftest.py create mode 100644 test/signal/test_signal.py delete mode 100644 test/signal_flow_graph/conftest.py diff --git a/b_asic/abstract_graph_component.py b/b_asic/abstract_graph_component.py index 6efb94e3..a0b71b41 100644 --- a/b_asic/abstract_graph_component.py +++ b/b_asic/abstract_graph_component.py @@ -5,6 +5,7 @@ TODO: More info. from b_asic.graph_component import GraphComponent, Name + class AbstractGraphComponent(GraphComponent): """Abstract Graph Component class which is a component of a signal flow graph. diff --git a/b_asic/abstract_operation.py b/b_asic/abstract_operation.py index 7cee1790..1403f7a9 100644 --- a/b_asic/abstract_operation.py +++ b/b_asic/abstract_operation.py @@ -4,7 +4,7 @@ TODO: More info. """ from abc import abstractmethod -from typing import List, Set, Dict, Optional, Any +from typing import List, Dict, Optional, Any from numbers import Number from b_asic.port import InputPort, OutputPort @@ -13,6 +13,8 @@ from b_asic.operation import Operation from b_asic.simulation import SimulationState, OperationState from b_asic.utilities import breadth_first_search from b_asic.abstract_graph_component import AbstractGraphComponent +from b_asic.graph_component import Name + class AbstractOperation(Operation, AbstractGraphComponent): """Generic abstract operation class which most implementations will derive from. @@ -23,15 +25,16 @@ class AbstractOperation(Operation, AbstractGraphComponent): _output_ports: List[OutputPort] _parameters: Dict[str, Optional[Any]] - def __init__(self, **kwds): - super().__init__(**kwds) + def __init__(self, name: Name = ""): + super().__init__(name) self._input_ports = [] self._output_ports = [] self._parameters = {} @abstractmethod def evaluate(self, inputs: list) -> list: - """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]: diff --git a/b_asic/core_operations.py b/b_asic/core_operations.py index f023e1a5..42867aa5 100644 --- a/b_asic/core_operations.py +++ b/b_asic/core_operations.py @@ -29,10 +29,10 @@ class Constant(AbstractOperation): TODO: More info. """ - def __init__(self, value: Number, **kwds): - """Construct a Constant.""" - super().__init__(**kwds) - self._output_ports = [OutputPort(1, self)] # TODO: Generate appropriate ID for ports. + def __init__(self, value: Number = 0, name: Name = ""): + super().__init__(name) + + self._output_ports = [OutputPort(0, self)] # TODO: Generate appropriate ID for ports. self._parameters["value"] = value def evaluate(self, inputs: list) -> list: @@ -48,11 +48,16 @@ class Addition(AbstractOperation): TODO: More info. """ - def __init__(self, **kwds): - """Construct an Addition.""" - super().__init__(**kwds) - self._input_ports = [InputPort(1, self), InputPort(1, self)] # TODO: Generate appropriate ID for ports. - self._output_ports = [OutputPort(1, self)] # TODO: Generate appropriate ID for ports. + def __init__(self, source1: OutputPort = None, source2: OutputPort = None, name: Name = ""): + super().__init__(name) + + self._input_ports = [InputPort(0, self), InputPort(1, self)] # TODO: Generate appropriate ID for ports. + self._output_ports = [OutputPort(0, self)] # TODO: Generate appropriate ID for ports. + + if source1 is not None: + self._input_ports[0].connect_to_port(source1) + if source2 is not None: + self._input_ports[1].connect_to_port(source2) def evaluate(self, inputs: list) -> list: return [inputs[0] + inputs[1]] @@ -67,13 +72,15 @@ class ConstantMultiplication(AbstractOperation): TODO: More info. """ - def __init__(self, coefficient: Number, **kwds): - """Construct a ConstantMultiplication.""" - super().__init__(**kwds) - self._input_ports = [InputPort(1), self] # TODO: Generate appropriate ID for ports. - self._output_ports = [OutputPort(1, self)] # TODO: Generate appropriate ID for ports. + def __init__(self, coefficient: Number, source1: OutputPort = None, name: Name = ""): + super().__init__(name) + self._input_ports = [InputPort(0, self)] # TODO: Generate appropriate ID for ports. + self._output_ports = [OutputPort(0, self)] # TODO: Generate appropriate ID for ports. self._parameters["coefficient"] = coefficient + if source1 is not None: + self._input_ports[0].connect_to_port(source1) + def evaluate(self, inputs: list) -> list: return [inputs[0] * self.param("coefficient")] diff --git a/b_asic/graph_component.py b/b_asic/graph_component.py index 4bce63f2..ec39a28d 100644 --- a/b_asic/graph_component.py +++ b/b_asic/graph_component.py @@ -18,17 +18,17 @@ class GraphComponent(ABC): @property @abstractmethod def type_name(self) -> TypeName: - """Returns the type name of the graph component""" + """Return the type name of the graph component""" raise NotImplementedError @property @abstractmethod def name(self) -> Name: - """Returns the name of the graph component.""" + """Return the name of the graph component.""" raise NotImplementedError @name.setter @abstractmethod def name(self, name: Name) -> None: - """Sets the name of the graph component to the entered name.""" + """Set the name of the graph component to the entered name.""" raise NotImplementedError diff --git a/b_asic/graph_id.py b/b_asic/graph_id.py index 0fd1855b..8da6a9d4 100644 --- a/b_asic/graph_id.py +++ b/b_asic/graph_id.py @@ -20,7 +20,7 @@ class GraphIDGenerator: self._next_id_number = defaultdict(lambda: 1) # Initalises every key element to 1 def get_next_id(self, graph_id_type: GraphIDType) -> GraphID: - """Returns the next graph id for a certain graph id type.""" + """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 4a716b79..acd26672 100644 --- a/b_asic/operation.py +++ b/b_asic/operation.py @@ -43,13 +43,11 @@ class Operation(GraphComponent): """Get the input port at index i.""" raise NotImplementedError - @abstractmethod def output(self, i: int) -> "OutputPort": """Get the output port at index i.""" raise NotImplementedError - @abstractmethod def params(self) -> Dict[str, Optional[Any]]: """Get a dictionary of all parameter values.""" diff --git a/b_asic/port.py b/b_asic/port.py index 6cbd59ba..eff9db9d 100644 --- a/b_asic/port.py +++ b/b_asic/port.py @@ -6,15 +6,16 @@ TODO: More info. from abc import ABC, abstractmethod from typing import NewType, Optional, List -from b_asic.signal import Signal from b_asic.operation import Operation +from b_asic.signal import Signal PortId = NewType("PortId", int) class Port(ABC): """Abstract port class. - TODO: More info. + + Handles functionality for port id and saves the connection to the parent operation. """ _port_id: PortId @@ -25,39 +26,62 @@ class Port(ABC): self._operation = operation @property - def identifier(self) -> PortId: - """Get the unique identifier.""" + def id(self) -> PortId: + """Return the unique portid.""" return self._port_id @property def operation(self) -> Operation: - """Get the connected operation.""" + """Return the connected operation.""" return self._operation @property @abstractmethod def signals(self) -> List[Signal]: - """Get a list of all connected signals.""" + """Return a list of all connected signals.""" raise NotImplementedError @abstractmethod def signal(self, i: int = 0) -> Signal: - """Get the connected signal at index i.""" + """Return the connected signal at index i. + + Keyword argumens: + i: integer index of the signal requsted. + """ + 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: - """Get the number of connected signals.""" + """Return the number of connected signals.""" + raise NotImplementedError + + @abstractmethod + def connect_port(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.""" raise NotImplementedError @abstractmethod - def connect(self, signal: Signal) -> None: - """Connect a signal.""" + def connect_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, i: int = 0) -> None: - """Disconnect a signal.""" + def disconnect_signal(self, i: int = 0) -> None: + """Disconnect a signal from the port. If the port is still connected to the entered signal + then the port is disconnected from the the entered signal aswell.""" + raise NotImplementedError + + @abstractmethod + def is_connected_to_signal(self, signal: Signal) -> bool: + """Return true if the port is connected to the entered signal else false.""" raise NotImplementedError @@ -65,34 +89,51 @@ class InputPort(Port): """Input port. TODO: More info. """ - _source_signal: Optional[Signal] + + _signal: Optional[Signal] def __init__(self, port_id: PortId, operation: Operation): super().__init__(port_id, operation) - self._source_signal = None + self._signal = None @property def signals(self) -> List[Signal]: - return [] if self._source_signal is None else [self._source_signal] + return [] if self._signal is None else [self._signal] def signal(self, i: int = 0) -> Signal: - assert 0 <= i < self.signal_count() # TODO: Error message. - assert self._source_signal is not None # TODO: Error message. - return self._source_signal + assert 0 <= i < self.signal_count(), "Signal index out of bound." + assert self._signal is not None, "No Signal connect to InputPort." + return self._signal + + @property + def connected_ports(self) -> List[Port]: + return [] if self._signal is None else [self._signal.source] def signal_count(self) -> int: - return 0 if self._source_signal is None else 1 + return 0 if self._signal is None else 1 - def connect(self, signal: Signal) -> None: - self._source_signal = signal - signal.destination = self + def connect_port(self, port: "OutputPort") -> Signal: + assert self._signal is None, "Connecting new port to already connected input port." + return Signal(port, self) # self._signal is set by the signal constructor - def disconnect(self, i: int = 0) -> None: - assert 0 <= i < self.signal_count() # TODO: Error message. - self._source_signal.disconnect_source() - self._source_signal = None + def connect_signal(self, signal: Signal) -> None: + assert self._signal is None, "Connecting new port to already connected input port." + self._signal = signal + if self is not signal.destination: + # Connect this inputport as destination for this signal if it isn't already. + signal.connect_destination(self) - # TODO: More stuff. + def disconnect_signal(self, i: int = 0) -> None: + assert 0 <= i < self.signal_count(), "Signal Index out of range." + old_signal: Signal = self._signal + self._signal = None + if self is old_signal.destination: + # Disconnect the dest of the signal if this inputport currently is the dest + old_signal.disconnect_destination() + old_signal.disconnect_destination() + + def is_connected_to_signal(self, signal: Signal) -> bool: + return self._signal is signal class OutputPort(Port): @@ -100,30 +141,57 @@ class OutputPort(Port): TODO: More info. """ - _destination_signals: List[Signal] + _signals: List[Signal] def __init__(self, port_id: PortId, operation: Operation): super().__init__(port_id, operation) - self._destination_signals = [] + self._signals = [] @property def signals(self) -> List[Signal]: - return self._destination_signals.copy() + return self._signals.copy() def signal(self, i: int = 0) -> Signal: - assert 0 <= i < self.signal_count() # TODO: Error message. - return self._destination_signals[i] - - def signal_count(self) -> int: - return len(self._destination_signals) + assert 0 <= i < self.signal_count(), "Signal index out of bounds." + return self._signals[i] - def connect(self, signal: Signal) -> None: - assert signal not in self._destination_signals # TODO: Error message. - self._destination_signals.append(signal) - signal.source = self - - def disconnect(self, i: int = 0) -> None: - assert 0 <= i < self.signal_count() # TODO: Error message. - del self._destination_signals[i] + @property + def connected_ports(self) -> List[Port]: + return [signal.destination for signal in self._signals \ + if signal.destination is not None] - # TODO: More stuff. + def signal_count(self) -> int: + return len(self._signals) + + def connect_port(self, port: InputPort) -> Signal: + return Signal(self, port) # Signal is added to self._signals in signal constructor + + def connect_signal(self, signal: Signal) -> None: + assert not self.is_connected_to_signal(signal), \ + "Attempting to connect to Signal already connected." + self._signals.append(signal) + if self is not signal.source: + # Connect this outputport to the signal if it isn't already + signal.connect_source(self) + + def disconnect_signal(self, i: int = 0) -> None: + assert 0 <= i < self.signal_count(), "Signal index out of bounds." + old_signal: Signal = self._signals[i] + del self._signals[i] + if self is old_signal.source: + # Disconnect the source of the signal if this outputport currently is the source + old_signal.disconnect_source() + + def disconnect_signal_by_ref(self, signal: Signal) -> None: + """Remove the signal that was entered from the OutputPorts signals. + If the entered signal still is connected to this port then disconnect the + entered signal from the port aswell. + + Keyword arguments: + - signal: Signal to remove. + """ + i: int = self._signals.index(signal) + self.disconnect_signal(i) + + def is_connected_to_signal(self, signal: Signal) -> bool: + return signal in self._signals # O(n) complexity diff --git a/b_asic/signal.py b/b_asic/signal.py index 4d80530e..917e4af3 100644 --- a/b_asic/signal.py +++ b/b_asic/signal.py @@ -1,52 +1,97 @@ """@package docstring B-ASIC Signal Module. """ -from typing import TYPE_CHECKING, Optional +from typing import Optional, TYPE_CHECKING from b_asic.graph_component import TypeName from b_asic.abstract_graph_component import AbstractGraphComponent +from b_asic.graph_component import Name if TYPE_CHECKING: - from b_asic import OutputPort, InputPort + from b_asic.port import InputPort, OutputPort class Signal(AbstractGraphComponent): """A connection between two ports.""" + _source: "OutputPort" _destination: "InputPort" - def __init__(self, src: Optional["OutputPort"] = None, dest: Optional["InputPort"] = None, **kwds): - super().__init__(**kwds) - self._source = src - self._destination = dest + def __init__(self, source: Optional["OutputPort"] = None, \ + destination: Optional["InputPort"] = None, name: Name = ""): + + super().__init__(name) + + self._source = source + self._destination = destination + + if source is not None: + self.connect_source(source) + + if destination is not None: + self.connect_destination(destination) @property def source(self) -> "OutputPort": - """Returns the source OutputPort of the signal.""" + """Return the source OutputPort of the signal.""" return self._source @property def destination(self) -> "InputPort": - """Returns the destination InputPort of the signal.""" + """Return the destination "InputPort" of the signal.""" return self._destination - @source.setter - def source(self, src: "OutputPort") -> None: - """Sets the value of the source OutputPort of the signal.""" + def connect_source(self, src: "OutputPort") -> None: + """Disconnect the previous source OutputPort of the signal and + connect to the entered source OutputPort. Also connect the entered + source port to the signal if it hasn't already been connected. + + Keyword arguments: + - src: OutputPort to connect as source to the signal. + """ + self.disconnect_source() self._source = src + if not src.is_connected_to_signal(self): + # If the new source isn't connected to this signal then connect it. + src.connect_signal(self) - @destination.setter - def destination(self, dest: "InputPort") -> None: - """Sets the value of the destination InputPort of the signal.""" + def connect_destination(self, dest: "InputPort") -> None: + """Disconnect the previous destination InputPort of the signal and + connect to the entered destination InputPort. Also connect the entered + destination port to the signal if it hasn't already been connected. + + Keywords argments: + - dest: InputPort to connect as destination to the signal. + """ + self.disconnect_destination() self._destination = dest + if not dest.is_connected_to_signal(self): + # If the new destination isn't connected to tis signal then connect it. + dest.connect_signal(self) @property def type_name(self) -> TypeName: return "s" def disconnect_source(self) -> None: - """Disconnects the source OutputPort of the signal.""" - self._source = 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 + self._source = None + if old_source.is_connected_to_signal(self): + # If the old destination port still is connected to this signal, then disconnect it. + old_source.disconnect_signal_by_ref(self) def disconnect_destination(self) -> None: - """Disconnects the destination InputPort of the signal.""" - self._destination = None + """Disconnect the destination InputPort of the signal.""" + if self._destination is not None: + old_destination: "InputPort" = self._destination + self._destination = None + if old_destination.is_connected_to_signal(self): + # If the old destination port still is connected to this signal, then disconnect it. + old_destination.disconnect_signal() + + def is_connected(self) -> bool: + """Returns true if the signal is connected to both a source and a destination, + else false.""" + return self._source is not None and self._destination is not None diff --git a/b_asic/signal_flow_graph.py b/b_asic/signal_flow_graph.py index 6a91521a..ab2c3e94 100644 --- a/b_asic/signal_flow_graph.py +++ b/b_asic/signal_flow_graph.py @@ -50,8 +50,8 @@ class SFG(AbstractOperation): return [] # TODO: Implement def _add_graph_component(self, graph_component: GraphComponent) -> GraphID: - """Adds the entered graph component to the SFG's dictionary of graph objects and - returns a generated GraphID for it. + """Add the entered graph component to the SFG's dictionary of graph objects and + return a generated GraphID for it. Keyword arguments: graph_component: Graph component to add to the graph. @@ -65,8 +65,8 @@ class SFG(AbstractOperation): return graph_id def find_by_id(self, graph_id: GraphID) -> Optional[GraphComponent]: - """Finds a graph object based on the entered Graph ID and returns it. If no graph - object with the entered ID was found then returns None. + """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. Keyword arguments: graph_id: Graph ID of the wanted object. @@ -77,8 +77,8 @@ class SFG(AbstractOperation): return None def find_by_name(self, name: Name) -> List[GraphComponent]: - """Finds all graph objects that have the entered name and returns them - in a list. If no graph object with the entered name was found then returns an + """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: diff --git a/test/fixtures/operation_tree.py b/test/fixtures/operation_tree.py index 7cc9e422..b97e89d7 100644 --- a/test/fixtures/operation_tree.py +++ b/test/fixtures/operation_tree.py @@ -10,9 +10,9 @@ def operation(): def create_operation(_type, dest_oper, index, **kwargs): oper = _type(**kwargs) oper_signal = Signal() - oper._output_ports[0].connect(oper_signal) + oper._output_ports[0].connect_signal(oper_signal) - dest_oper._input_ports[index].connect(oper_signal) + dest_oper._input_ports[index].connect_signal(oper_signal) return oper @pytest.fixture @@ -50,11 +50,11 @@ def large_operation_tree(): create_operation(Constant, add_oper_2, 1, value=5) add_oper_3 = Addition() - add_oper_signal = Signal(add_oper, add_oper_3) - add_oper._output_ports[0].connect(add_oper_signal) - add_oper_3._input_ports[0].connect(add_oper_signal) + add_oper_signal = Signal(add_oper.output(0), add_oper_3.output(0)) + add_oper._output_ports[0].connect_signal(add_oper_signal) + add_oper_3._input_ports[0].connect_signal(add_oper_signal) - add_oper_2_signal = Signal(add_oper_2, add_oper_3) - add_oper_2._output_ports[0].connect(add_oper_2_signal) - add_oper_3._input_ports[1].connect(add_oper_2_signal) + add_oper_2_signal = Signal(add_oper_2.output(0), add_oper_3.output(0)) + add_oper_2._output_ports[0].connect_signal(add_oper_2_signal) + add_oper_3._input_ports[1].connect_signal(add_oper_2_signal) return const_oper diff --git a/test/graph_id/conftest.py b/test/graph_id/conftest.py deleted file mode 100644 index 5871ed8e..00000000 --- a/test/graph_id/conftest.py +++ /dev/null @@ -1 +0,0 @@ -import pytest diff --git a/test/port/test_inputport.py b/test/port/test_inputport.py index f0e70cb7..7a78d5f7 100644 --- a/test/port/test_inputport.py +++ b/test/port/test_inputport.py @@ -2,19 +2,83 @@ B-ASIC test suite for Inputport """ -from b_asic import InputPort - import pytest -def test_connect_multiple_signals(signals): - """ - test if only one signal can connect to an input port - """ +from b_asic import InputPort, OutputPort +from b_asic import 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) + +@pytest.fixture +def dangling_sig(): + return Signal() + +@pytest.fixture +def s_w_source(): + out_port = OutputPort(0, None) + return Signal(source=out_port) + +@pytest.fixture +def sig_with_dest(): + inp_port = InputPort(0, None) + return Signal(destination=out_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 test_connect_port_then_disconnect(inp_port, out_port): + """Test connect unused port to port.""" + s1 = inp_port.connect_port(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.disconnect_signal() + + 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 + 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_port(out_port) + with pytest.raises(AssertionError): + inp_port.connect_port(out_port2) + +def test_connect_signal_then_disconnect(inp_port, s_w_source): + inp_port.connect_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 s_w_source.source.signals == [s_w_source] + assert s_w_source.destination is inp_port - for s in signals: - inp_port.connect(s) + inp_port.disconnect_signal() - assert inp_port.signal_count() == 1 - assert inp_port.signals[0] == signals[-1] + assert inp_port.connected_ports == [] + assert s_w_source.source.connected_ports == [] + assert inp_port.signals == [] + assert s_w_source.source.signals == [s_w_source] + assert s_w_source.destination is None diff --git a/test/port/test_outputport.py b/test/port/test_outputport.py index 6ca126d5..f48afbdb 100644 --- a/test/port/test_outputport.py +++ b/test/port/test_outputport.py @@ -1,18 +1,31 @@ """ -B-ASIC test suite for InputPort +B-ASIC test suite for OutputPort TODO: More info """ -from b_asic import OutputPort +from b_asic import InputPort, OutputPort import pytest -def test_connect_multiple_signals(signals): - """ - test if multiple signals can connect to an output port - """ - outp_port = OutputPort(0, None) +@pytest.fixture +def inp_ports(): + return [InputPort(_, None) for _ in range(0,3)] - for s in signals: - outp_port.connect(s) +def test_connect_multiple_signals(inp_ports): + """Can multiple ports connect to an output port?""" + out_port = OutputPort(0, None) - assert outp_port.signal_count() == 3 - assert outp_port.signals == signals + for port in inp_ports: + out_port.connect_port(port) + + assert out_port.signal_count() == len(inp_ports) + +def test_disconnect_multiple_signals(inp_ports): + """Can multiple ports disconnect from an output port?""" + out_port = OutputPort(0, None) + + for port in inp_ports: + out_port.connect_port(port) + + for _ in inp_ports: + out_port.disconnect_signal(0) + + assert out_port.signal_count() == 0 \ No newline at end of file diff --git a/test/signal/test_signal.py b/test/signal/test_signal.py new file mode 100644 index 00000000..8c10d1e3 --- /dev/null +++ b/test/signal/test_signal.py @@ -0,0 +1,62 @@ +""" +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 + +def test_signal_creation_and_disconnction_and_connection_changing(): + in_port = InputPort(0, None) + out_port = OutputPort(1, None) + s = Signal(out_port, in_port) + + assert in_port.signals == [s] + assert out_port.signals == [s] + assert s.source is out_port + assert s.destination is in_port + + in_port1 = InputPort(0, None) + s.connect_destination(in_port1) + + assert in_port.signals == [] + assert in_port1.signals == [s] + assert out_port.signals == [s] + assert s.source is out_port + assert s.destination is in_port1 + + s.disconnect_source() + + assert out_port.signals == [] + assert in_port1.signals == [s] + assert s.source is None + assert s.destination is in_port1 + + s.disconnect_destination() + + assert out_port.signals == [] + assert in_port1.signals == [] + assert s.source is None + assert s.destination is None + + out_port1 = OutputPort(0, None) + s.connect_source(out_port1) + + assert out_port1.signals == [s] + assert s.source is out_port1 + assert s.destination is None + + s.connect_source(out_port) + + assert out_port.signals == [s] + assert out_port1.signals == [] + assert s.source is out_port + assert s.destination is None + + s.connect_destination(in_port) + + assert out_port.signals == [s] + assert in_port.signals == [s] + assert s.source is out_port + assert s.destination is in_port diff --git a/test/signal_flow_graph/conftest.py b/test/signal_flow_graph/conftest.py deleted file mode 100644 index 5871ed8e..00000000 --- a/test/signal_flow_graph/conftest.py +++ /dev/null @@ -1 +0,0 @@ -import pytest diff --git a/test/traverse/test_traverse_tree.py b/test/traverse/test_traverse_tree.py index 2c1d08fe..9f509287 100644 --- a/test/traverse/test_traverse_tree.py +++ b/test/traverse/test_traverse_tree.py @@ -24,6 +24,7 @@ def test_traverse_type(large_operation_tree): def test_traverse_loop(operation_tree): add_oper_signal = Signal() - operation_tree._output_ports[0].connect(add_oper_signal) - operation_tree._input_ports[0].connect(add_oper_signal) + operation_tree._output_ports[0].connect_signal(add_oper_signal) + operation_tree._input_ports[0].disconnect_signal() + operation_tree._input_ports[0].connect_signal(add_oper_signal) assert len(list(operation_tree.traverse())) == 2 -- GitLab From 9494fe85d9d7d1c0e49e9432036d90d1765b9ad1 Mon Sep 17 00:00:00 2001 From: Arvid Westerlund <arvwe160@student.liu.se> Date: Sat, 14 Mar 2020 23:51:11 +0100 Subject: [PATCH 46/50] Resolve "Basic Operations" --- b_asic/abstract_operation.py | 2 +- b_asic/core_operations.py | 288 ++++++++++++++++-- .../basic_operations/test_basic_operations.py | 229 ++++++++++++++ 3 files changed, 500 insertions(+), 19 deletions(-) create mode 100644 test/basic_operations/test_basic_operations.py diff --git a/b_asic/abstract_operation.py b/b_asic/abstract_operation.py index 1403f7a9..fc3a9205 100644 --- a/b_asic/abstract_operation.py +++ b/b_asic/abstract_operation.py @@ -32,7 +32,7 @@ class AbstractOperation(Operation, AbstractGraphComponent): self._parameters = {} @abstractmethod - def evaluate(self, inputs: list) -> list: + 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.""" raise NotImplementedError diff --git a/b_asic/core_operations.py b/b_asic/core_operations.py index 42867aa5..f64c63db 100644 --- a/b_asic/core_operations.py +++ b/b_asic/core_operations.py @@ -4,15 +4,14 @@ TODO: More info. """ from numbers import Number - +from typing import Any +from numpy import conjugate, sqrt, abs as np_abs from b_asic.port import InputPort, OutputPort -from b_asic.operation import Operation +from b_asic.graph_id import GraphIDType from b_asic.abstract_operation import AbstractOperation -from b_asic.abstract_graph_component import AbstractGraphComponent from b_asic.graph_component import Name, TypeName - -class Input(Operation, AbstractGraphComponent): +class Input(AbstractOperation): """Input operation. TODO: More info. """ @@ -24,6 +23,7 @@ class Input(Operation, AbstractGraphComponent): return "in" + class Constant(AbstractOperation): """Constant value operation. TODO: More info. @@ -32,15 +32,16 @@ class Constant(AbstractOperation): def __init__(self, value: Number = 0, name: Name = ""): super().__init__(name) - self._output_ports = [OutputPort(0, self)] # TODO: Generate appropriate ID for ports. + self._output_ports = [OutputPort(0, self)] self._parameters["value"] = value - def evaluate(self, inputs: list) -> list: - return [self.param("value")] + def evaluate(self) -> Any: + return self.param("value") @property def type_name(self) -> TypeName: - return "const" + return "c" + class Addition(AbstractOperation): @@ -51,22 +52,207 @@ class Addition(AbstractOperation): def __init__(self, source1: OutputPort = None, source2: OutputPort = None, name: Name = ""): super().__init__(name) - self._input_ports = [InputPort(0, self), InputPort(1, self)] # TODO: Generate appropriate ID for ports. - self._output_ports = [OutputPort(0, self)] # TODO: Generate appropriate ID for ports. + self._input_ports = [InputPort(0, self), InputPort(0, self)] + self._output_ports = [OutputPort(0, self)] if source1 is not None: self._input_ports[0].connect_to_port(source1) if source2 is not None: self._input_ports[1].connect_to_port(source2) - def evaluate(self, inputs: list) -> list: - return [inputs[0] + inputs[1]] + def evaluate(self, a, b) -> Any: + return a + b @property def type_name(self) -> TypeName: return "add" +class Subtraction(AbstractOperation): + """Binary subtraction 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(0, self)] + self._output_ports = [OutputPort(0, self)] + + if source1 is not None: + self._input_ports[0].connect_to_port(source1) + if source2 is not None: + self._input_ports[1].connect_to_port(source2) + + def evaluate(self, a, b) -> Any: + return a - b + + @property + def type_name(self) -> GraphIDType: + return "sub" + + +class Multiplication(AbstractOperation): + """Binary multiplication 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(0, self)] + self._output_ports = [OutputPort(0, self)] + + if source1 is not None: + self._input_ports[0].connect_to_port(source1) + if source2 is not None: + self._input_ports[1].connect_to_port(source2) + + def evaluate(self, a, b) -> Any: + return a * b + + @property + def type_name(self) -> GraphIDType: + 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(0, self)] + self._output_ports = [OutputPort(0, self)] + + if source1 is not None: + self._input_ports[0].connect_to_port(source1) + if source2 is not None: + self._input_ports[1].connect_to_port(source2) + + def evaluate(self, a, b) -> Any: + return a / b + + @property + def type_name(self) -> GraphIDType: + return "div" + + +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_to_port(source1) + + + def evaluate(self, a) -> Any: + return sqrt((complex)(a)) + + @property + def type_name(self) -> GraphIDType: + return "sqrt" + + +class ComplexConjugate(AbstractOperation): + """Unary complex conjugate 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_to_port(source1) + + + def evaluate(self, a) -> Any: + return conjugate(a) + + @property + def type_name(self) -> GraphIDType: + return "conj" + + +class Max(AbstractOperation): + """Binary max 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(0, self)] + self._output_ports = [OutputPort(0, self)] + + if source1 is not None: + self._input_ports[0].connect_to_port(source1) + if source2 is not None: + self._input_ports[1].connect_to_port(source2) + + def evaluate(self, a, b) -> Any: + assert not isinstance(a, complex) and not isinstance(b, complex), \ + ("core_operation.Max does not support complex numbers.") + return a if a > b else b + + @property + def type_name(self) -> GraphIDType: + return "max" + + +class Min(AbstractOperation): + """Binary min 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(0, self)] + self._output_ports = [OutputPort(0, self)] + + if source1 is not None: + self._input_ports[0].connect_to_port(source1) + if source2 is not None: + self._input_ports[1].connect_to_port(source2) + + def evaluate(self, a, b) -> Any: + assert not isinstance(a, complex) and not isinstance(b, complex), \ + ("core_operation.Min does not support complex numbers.") + return a if a < b else b + + @property + def type_name(self) -> GraphIDType: + return "min" + + +class Absolute(AbstractOperation): + """Unary absolute value 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_to_port(source1) + + + def evaluate(self, a) -> Any: + return np_abs(a) + + @property + def type_name(self) -> GraphIDType: + return "abs" + + class ConstantMultiplication(AbstractOperation): """Unary constant multiplication operation. TODO: More info. @@ -74,16 +260,82 @@ class ConstantMultiplication(AbstractOperation): def __init__(self, coefficient: Number, source1: OutputPort = None, name: Name = ""): super().__init__(name) - self._input_ports = [InputPort(0, self)] # TODO: Generate appropriate ID for ports. - self._output_ports = [OutputPort(0, self)] # TODO: Generate appropriate ID for ports. + self._input_ports = [InputPort(0, self)] + self._output_ports = [OutputPort(0, self)] self._parameters["coefficient"] = coefficient if source1 is not None: self._input_ports[0].connect_to_port(source1) - def evaluate(self, inputs: list) -> list: - return [inputs[0] * self.param("coefficient")] + def evaluate(self, a) -> Any: + return a * self.param("coefficient") @property def type_name(self) -> TypeName: - return "const_mul" + return "cmul" + + +class ConstantAddition(AbstractOperation): + """Unary constant addition operation. + TODO: More info. + """ + + 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 + + if source1 is not None: + self._input_ports[0].connect_to_port(source1) + + def evaluate(self, a) -> Any: + return a + self.param("coefficient") + + @property + def type_name(self) -> GraphIDType: + return "cadd" + + +class ConstantSubtraction(AbstractOperation): + """Unary constant subtraction operation. + TODO: More info. + """ + + 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 + + if source1 is not None: + self._input_ports[0].connect_to_port(source1) + + def evaluate(self, a) -> Any: + return a - self.param("coefficient") + + @property + def type_name(self) -> GraphIDType: + return "csub" + + +class ConstantDivision(AbstractOperation): + """Unary constant division operation. + TODO: More info. + """ + + 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 + + if source1 is not None: + self._input_ports[0].connect_to_port(source1) + + def evaluate(self, a) -> Any: + return a / self.param("coefficient") + + @property + def type_name(self) -> GraphIDType: + return "cdiv" diff --git a/test/basic_operations/test_basic_operations.py b/test/basic_operations/test_basic_operations.py new file mode 100644 index 00000000..21561074 --- /dev/null +++ b/test/basic_operations/test_basic_operations.py @@ -0,0 +1,229 @@ +""" +B-ASIC test suite for the basic operations. +""" + +from b_asic.core_operations import Constant, Addition, Subtraction, Multiplication, Division, SquareRoot, ComplexConjugate, Max, Min, Absolute, ConstantMultiplication, ConstantAddition, ConstantSubtraction, ConstantDivision +from b_asic.signal import Signal +import pytest + +""" 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) \ No newline at end of file -- GitLab From 8b7975190baae9b496d4055c5227d314bddfe2f0 Mon Sep 17 00:00:00 2001 From: Angus Lothian <anglo547@student.liu.se> Date: Wed, 18 Mar 2020 20:36:36 +0100 Subject: [PATCH 47/50] Solve pull request comments and change so evaluate function in SFG uses the same interface as the abstract evaluate function --- b_asic/__init__.py | 3 - b_asic/abstract_graph_component.py | 26 --- b_asic/abstract_operation.py | 117 ----------- b_asic/core_operations.py | 17 +- b_asic/graph_component.py | 20 ++ b_asic/operation.py | 120 ++++++++++- b_asic/port.py | 192 ++++++++++-------- b_asic/signal.py | 37 ++-- b_asic/signal_flow_graph.py | 4 +- b_asic/utilities.py | 21 -- .../test_core_operations.py} | 32 +-- test/fixtures/operation_tree.py | 12 +- test/port/test_inputport.py | 27 ++- test/port/test_outputport.py | 31 ++- test/signal/test_signal.py | 12 +- test/traverse/test_traverse_tree.py | 6 +- 16 files changed, 347 insertions(+), 330 deletions(-) delete mode 100644 b_asic/abstract_graph_component.py delete mode 100644 b_asic/abstract_operation.py delete mode 100644 b_asic/utilities.py rename test/{basic_operations/test_basic_operations.py => core_operations/test_core_operations.py} (94%) diff --git a/b_asic/__init__.py b/b_asic/__init__.py index fc787edf..7e40ad52 100644 --- a/b_asic/__init__.py +++ b/b_asic/__init__.py @@ -2,8 +2,6 @@ Better ASIC Toolbox. TODO: More info. """ -from b_asic.abstract_graph_component import * -from b_asic.abstract_operation import * from b_asic.core_operations import * from b_asic.graph_component import * from b_asic.graph_id import * @@ -14,4 +12,3 @@ 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.utilities import * diff --git a/b_asic/abstract_graph_component.py b/b_asic/abstract_graph_component.py deleted file mode 100644 index a0b71b41..00000000 --- a/b_asic/abstract_graph_component.py +++ /dev/null @@ -1,26 +0,0 @@ -"""@package docstring -B-ASIC module for Graph Components of a signal flow graph. -TODO: More info. -""" - -from b_asic.graph_component import GraphComponent, Name - - -class AbstractGraphComponent(GraphComponent): - """Abstract Graph Component class which is a component of a signal flow graph. - - TODO: More info. - """ - - _name: Name - - def __init__(self, name: Name = ""): - self._name = name - - @property - def name(self) -> Name: - return self._name - - @name.setter - def name(self, name: Name) -> None: - self._name = name diff --git a/b_asic/abstract_operation.py b/b_asic/abstract_operation.py deleted file mode 100644 index fc3a9205..00000000 --- a/b_asic/abstract_operation.py +++ /dev/null @@ -1,117 +0,0 @@ -"""@package docstring -B-ASIC Abstract Operation Module. -TODO: More info. -""" - -from abc import abstractmethod -from typing import List, Dict, Optional, Any -from numbers import Number - -from b_asic.port import InputPort, OutputPort -from b_asic.signal import Signal -from b_asic.operation import Operation -from b_asic.simulation import SimulationState, OperationState -from b_asic.utilities import breadth_first_search -from b_asic.abstract_graph_component import AbstractGraphComponent -from b_asic.graph_component import Name - - -class AbstractOperation(Operation, AbstractGraphComponent): - """Generic abstract operation class which most implementations will derive from. - TODO: More info. - """ - - _input_ports: List[InputPort] - _output_ports: List[OutputPort] - _parameters: Dict[str, Optional[Any]] - - def __init__(self, name: Name = ""): - super().__init__(name) - self._input_ports = [] - self._output_ports = [] - self._parameters = {} - - @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.""" - raise NotImplementedError - - def inputs(self) -> List[InputPort]: - return self._input_ports.copy() - - def outputs(self) -> List[OutputPort]: - return self._output_ports.copy() - - def input_count(self) -> int: - return len(self._input_ports) - - 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) - assert len(self_state.output_values) == output_count # TODO: Error message. - 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] - - @property - def neighbours(self) -> List[Operation]: - neighbours: List[Operation] = [] - for port in self._input_ports: - for signal in port.signals: - neighbours.append(signal.source.operation) - - for port in self._output_ports: - for signal in port.signals: - neighbours.append(signal.destination.operation) - - return neighbours - - def traverse(self) -> Operation: - """Traverse the operation tree and return a generator with start point in the operation.""" - return breadth_first_search(self) - - # TODO: More stuff. diff --git a/b_asic/core_operations.py b/b_asic/core_operations.py index f64c63db..ce1019f3 100644 --- a/b_asic/core_operations.py +++ b/b_asic/core_operations.py @@ -8,9 +8,10 @@ from typing import Any 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.abstract_operation import AbstractOperation +from b_asic.operation import AbstractOperation from b_asic.graph_component import Name, TypeName + class Input(AbstractOperation): """Input operation. TODO: More info. @@ -23,7 +24,6 @@ class Input(AbstractOperation): return "in" - class Constant(AbstractOperation): """Constant value operation. TODO: More info. @@ -43,7 +43,6 @@ class Constant(AbstractOperation): return "c" - class Addition(AbstractOperation): """Binary addition operation. TODO: More info. @@ -52,7 +51,7 @@ class Addition(AbstractOperation): def __init__(self, source1: OutputPort = None, source2: OutputPort = None, name: Name = ""): super().__init__(name) - self._input_ports = [InputPort(0, self), InputPort(0, self)] + self._input_ports = [InputPort(0, self), InputPort(1, self)] self._output_ports = [OutputPort(0, self)] if source1 is not None: @@ -75,7 +74,7 @@ class Subtraction(AbstractOperation): def __init__(self, source1: OutputPort = None, source2: OutputPort = None, name: Name = ""): super().__init__(name) - self._input_ports = [InputPort(0, self), InputPort(0, self)] + self._input_ports = [InputPort(0, self), InputPort(1, self)] self._output_ports = [OutputPort(0, self)] if source1 is not None: @@ -98,7 +97,7 @@ class Multiplication(AbstractOperation): def __init__(self, source1: OutputPort = None, source2: OutputPort = None, name: Name = ""): super().__init__(name) - self._input_ports = [InputPort(0, self), InputPort(0, self)] + self._input_ports = [InputPort(0, self), InputPort(1, self)] self._output_ports = [OutputPort(0, self)] if source1 is not None: @@ -121,7 +120,7 @@ class Division(AbstractOperation): def __init__(self, source1: OutputPort = None, source2: OutputPort = None, name: Name = ""): super().__init__(name) - self._input_ports = [InputPort(0, self), InputPort(0, self)] + self._input_ports = [InputPort(0, self), InputPort(1, self)] self._output_ports = [OutputPort(0, self)] if source1 is not None: @@ -188,7 +187,7 @@ class Max(AbstractOperation): def __init__(self, source1: OutputPort = None, source2: OutputPort = None, name: Name = ""): super().__init__(name) - self._input_ports = [InputPort(0, self), InputPort(0, self)] + self._input_ports = [InputPort(0, self), InputPort(1, self)] self._output_ports = [OutputPort(0, self)] if source1 is not None: @@ -213,7 +212,7 @@ class Min(AbstractOperation): def __init__(self, source1: OutputPort = None, source2: OutputPort = None, name: Name = ""): super().__init__(name) - self._input_ports = [InputPort(0, self), InputPort(0, self)] + self._input_ports = [InputPort(0, self), InputPort(1, self)] self._output_ports = [OutputPort(0, self)] if source1 is not None: diff --git a/b_asic/graph_component.py b/b_asic/graph_component.py index ec39a28d..1987d449 100644 --- a/b_asic/graph_component.py +++ b/b_asic/graph_component.py @@ -32,3 +32,23 @@ class GraphComponent(ABC): def name(self, name: Name) -> None: """Set the name of the graph component to the entered name.""" raise NotImplementedError + + +class AbstractGraphComponent(GraphComponent): + """Abstract Graph Component class which is a component of a signal flow graph. + + TODO: More info. + """ + + _name: Name + + def __init__(self, name: Name = ""): + self._name = name + + @property + def name(self) -> Name: + return self._name + + @name.setter + def name(self, name: Name) -> None: + self._name = name diff --git a/b_asic/operation.py b/b_asic/operation.py index acd26672..75644b73 100644 --- a/b_asic/operation.py +++ b/b_asic/operation.py @@ -5,13 +5,16 @@ TODO: More info. from abc import abstractmethod from numbers import Number -from typing import List, Dict, Optional, Any, TYPE_CHECKING +from typing import List, Dict, Optional, Any, Set, TYPE_CHECKING +from collections import deque -from b_asic.graph_component import GraphComponent +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 - from b_asic.simulation import SimulationState + class Operation(GraphComponent): """Operation interface. @@ -88,3 +91,114 @@ class Operation(GraphComponent): If no neighbours are found this returns an empty list """ raise NotImplementedError + + +class AbstractOperation(Operation, AbstractGraphComponent): + """Generic abstract operation class which most implementations will derive from. + TODO: More info. + """ + + _input_ports: List["InputPort"] + _output_ports: List["OutputPort"] + _parameters: Dict[str, Optional[Any]] + + def __init__(self, name: Name = ""): + super().__init__(name) + self._input_ports = [] + self._output_ports = [] + self._parameters = {} + + @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.""" + raise NotImplementedError + + def inputs(self) -> List["InputPort"]: + return self._input_ports.copy() + + def outputs(self) -> List["OutputPort"]: + return self._output_ports.copy() + + def input_count(self) -> int: + return len(self._input_ports) + + 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) + assert len(self_state.output_values) == output_count # TODO: Error message. + 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] + + @property + def neighbours(self) -> List[Operation]: + neighbours: List[Operation] = [] + for port in self._input_ports: + for signal in port.signals: + neighbours.append(signal.source.operation) + + for port in self._output_ports: + for signal in port.signals: + neighbours.append(signal.destination.operation) + + return neighbours + + 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.neighbours: + if n_operation not in visited: + visited.add(n_operation) + queue.append(n_operation) diff --git a/b_asic/port.py b/b_asic/port.py index eff9db9d..c22053df 100644 --- a/b_asic/port.py +++ b/b_asic/port.py @@ -9,31 +9,25 @@ from typing import NewType, Optional, List from b_asic.operation import Operation from b_asic.signal import Signal -PortId = NewType("PortId", int) - +PortIndex = NewType("PortIndex", int) class Port(ABC): - """Abstract port class. + """Port Interface. - Handles functionality for port id and saves the connection to the parent operation. + TODO: More documentaiton? """ - _port_id: PortId - _operation: Operation - - def __init__(self, port_id: PortId, operation: Operation): - self._port_id = port_id - self._operation = operation - - @property - def id(self) -> PortId: - """Return the unique portid.""" - return self._port_id - @property + @abstractmethod def operation(self) -> Operation: """Return the connected operation.""" - return self._operation + raise NotImplementedError + + @property + @abstractmethod + def index(self) -> PortIndex: + """Return the unique PortIndex.""" + raise NotImplementedError @property @abstractmethod @@ -62,136 +56,168 @@ class Port(ABC): raise NotImplementedError @abstractmethod - def connect_port(self, port: "Port") -> Signal: + 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.""" raise NotImplementedError @abstractmethod - def connect_signal(self, signal: Signal) -> None: + 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_signal(self, i: int = 0) -> None: - """Disconnect a signal from the port. If the port is still connected to the entered signal - then the port is disconnected from the the entered signal aswell.""" + 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.""" raise NotImplementedError @abstractmethod - def is_connected_to_signal(self, signal: Signal) -> bool: - """Return true if the port is connected to the entered signal else false.""" + 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. + + Keyword arguments: + - signal: Signal to remove. + """ + raise NotImplementedError + + @abstractmethod + def clear(self) -> None: + """Removes all connected signals from the Port.""" raise NotImplementedError -class InputPort(Port): +class AbstractPort(Port): + """Abstract port class. + + Handles functionality for port id and saves the connection to the parent operation. + """ + + _index: int + _operation: Operation + + def __init__(self, index: int, operation: Operation): + self._index = index + self._operation = operation + + @property + def operation(self) -> Operation: + return self._operation + + @property + def index(self) -> PortIndex: + return self._index + + +class InputPort(AbstractPort): """Input port. TODO: More info. """ - _signal: Optional[Signal] + _source_signal: Optional[Signal] - def __init__(self, port_id: PortId, operation: Operation): + def __init__(self, port_id: PortIndex, operation: Operation): super().__init__(port_id, operation) - self._signal = None + self._source_signal = None @property def signals(self) -> List[Signal]: - return [] if self._signal is None else [self._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._signal is not None, "No Signal connect to InputPort." - return self._signal + 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._signal is None else [self._signal.source] + 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._signal is None else 1 + return 0 if self._source_signal is None else 1 - def connect_port(self, port: "OutputPort") -> Signal: - assert self._signal is None, "Connecting new port to already connected input port." - return Signal(port, self) # self._signal is set by the signal constructor + 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 - def connect_signal(self, signal: Signal) -> None: - assert self._signal is None, "Connecting new port to already connected input port." - self._signal = 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.connect_destination(self) + signal.set_destination(self) - def disconnect_signal(self, i: int = 0) -> None: - assert 0 <= i < self.signal_count(), "Signal Index out of range." - old_signal: Signal = self._signal - self._signal = None + 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() + + def remove_signal(self, signal: Signal) -> None: + old_signal: Signal = self._source_signal + 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.disconnect_destination() - old_signal.disconnect_destination() - - def is_connected_to_signal(self, signal: Signal) -> bool: - return self._signal is signal + old_signal.remove_destination() + def clear(self) -> None: + self.remove_signal(self._source_signal) -class OutputPort(Port): +class OutputPort(AbstractPort): """Output port. TODO: More info. """ - _signals: List[Signal] + _destination_signals: List[Signal] - def __init__(self, port_id: PortId, operation: Operation): + def __init__(self, port_id: PortIndex, operation: Operation): super().__init__(port_id, operation) - self._signals = [] + self._destination_signals = [] @property def signals(self) -> List[Signal]: - return self._signals.copy() + 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._signals[i] + return self._destination_signals[i] @property def connected_ports(self) -> List[Port]: - return [signal.destination for signal in self._signals \ + return [signal.destination for signal in self._destination_signals \ if signal.destination is not None] def signal_count(self) -> int: - return len(self._signals) + return len(self._destination_signals) - def connect_port(self, port: InputPort) -> Signal: - return Signal(self, port) # Signal is added to self._signals in signal constructor + def connect(self, port: InputPort) -> Signal: + return Signal(self, port) # Signal is added to self._destination_signals in signal constructor - def connect_signal(self, signal: Signal) -> None: - assert not self.is_connected_to_signal(signal), \ + def add_signal(self, signal: Signal) -> None: + assert signal not in self.signals, \ "Attempting to connect to Signal already connected." - self._signals.append(signal) + self._destination_signals.append(signal) if self is not signal.source: # Connect this outputport to the signal if it isn't already - signal.connect_source(self) - - def disconnect_signal(self, i: int = 0) -> None: - assert 0 <= i < self.signal_count(), "Signal index out of bounds." - old_signal: Signal = self._signals[i] - del self._signals[i] + 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 + + 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: - # Disconnect the source of the signal if this outputport currently is the source - old_signal.disconnect_source() - - def disconnect_signal_by_ref(self, signal: Signal) -> None: - """Remove the signal that was entered from the OutputPorts signals. - If the entered signal still is connected to this port then disconnect the - entered signal from the port aswell. - - Keyword arguments: - - signal: Signal to remove. - """ - i: int = self._signals.index(signal) - self.disconnect_signal(i) + old_signal.remove_source() - def is_connected_to_signal(self, signal: Signal) -> bool: - return signal in self._signals # O(n) complexity + def clear(self) -> None: + for signal in self._destination_signals: + self.remove_signal(signal) diff --git a/b_asic/signal.py b/b_asic/signal.py index 917e4af3..64c25948 100644 --- a/b_asic/signal.py +++ b/b_asic/signal.py @@ -3,13 +3,12 @@ B-ASIC Signal Module. """ from typing import Optional, TYPE_CHECKING -from b_asic.graph_component import TypeName -from b_asic.abstract_graph_component import AbstractGraphComponent -from b_asic.graph_component import Name +from b_asic.graph_component import AbstractGraphComponent, TypeName, Name if TYPE_CHECKING: from b_asic.port import InputPort, OutputPort + class Signal(AbstractGraphComponent): """A connection between two ports.""" @@ -25,10 +24,10 @@ class Signal(AbstractGraphComponent): self._destination = destination if source is not None: - self.connect_source(source) + self.set_source(source) if destination is not None: - self.connect_destination(destination) + self.set_destination(destination) @property def source(self) -> "OutputPort": @@ -40,7 +39,7 @@ class Signal(AbstractGraphComponent): """Return the destination "InputPort" of the signal.""" return self._destination - def connect_source(self, src: "OutputPort") -> None: + def set_source(self, src: "OutputPort") -> None: """Disconnect the previous source OutputPort of the signal and connect to the entered source OutputPort. Also connect the entered source port to the signal if it hasn't already been connected. @@ -48,13 +47,13 @@ class Signal(AbstractGraphComponent): Keyword arguments: - src: OutputPort to connect as source to the signal. """ - self.disconnect_source() + self.remove_source() self._source = src - if not src.is_connected_to_signal(self): + if self not in src.signals: # If the new source isn't connected to this signal then connect it. - src.connect_signal(self) + src.add_signal(self) - def connect_destination(self, dest: "InputPort") -> None: + def set_destination(self, dest: "InputPort") -> None: """Disconnect the previous destination InputPort of the signal and connect to the entered destination InputPort. Also connect the entered destination port to the signal if it hasn't already been connected. @@ -62,34 +61,34 @@ class Signal(AbstractGraphComponent): Keywords argments: - dest: InputPort to connect as destination to the signal. """ - self.disconnect_destination() + self.remove_destination() self._destination = dest - if not dest.is_connected_to_signal(self): + if self not in dest.signals: # If the new destination isn't connected to tis signal then connect it. - dest.connect_signal(self) + dest.add_signal(self) @property def type_name(self) -> TypeName: return "s" - def disconnect_source(self) -> None: + 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 self._source = None - if old_source.is_connected_to_signal(self): + if self in old_source.signals: # If the old destination port still is connected to this signal, then disconnect it. - old_source.disconnect_signal_by_ref(self) + old_source.remove_signal(self) - def disconnect_destination(self) -> None: + def remove_destination(self) -> None: """Disconnect the destination InputPort of the signal.""" if self._destination is not None: old_destination: "InputPort" = self._destination self._destination = None - if old_destination.is_connected_to_signal(self): + if self in old_destination.signals: # If the old destination port still is connected to this signal, then disconnect it. - old_destination.disconnect_signal() + old_destination.remove_signal(self) def is_connected(self) -> bool: """Returns true if the signal is connected to both a source and a destination, diff --git a/b_asic/signal_flow_graph.py b/b_asic/signal_flow_graph.py index ab2c3e94..9c08aecc 100644 --- a/b_asic/signal_flow_graph.py +++ b/b_asic/signal_flow_graph.py @@ -7,7 +7,7 @@ from typing import List, Dict, Optional, DefaultDict from collections import defaultdict from b_asic.operation import Operation -from b_asic.abstract_operation import AbstractOperation +from b_asic.operation import AbstractOperation from b_asic.signal import Signal from b_asic.graph_id import GraphIDGenerator, GraphID from b_asic.graph_component import GraphComponent, Name, TypeName @@ -46,7 +46,7 @@ class SFG(AbstractOperation): # TODO: Traverse the graph between the inputs/outputs and add to self._operations. # TODO: Connect ports with signals with appropriate IDs. - def evaluate(self, inputs: list) -> list: + def evaluate(self, *inputs) -> list: return [] # TODO: Implement def _add_graph_component(self, graph_component: GraphComponent) -> GraphID: diff --git a/b_asic/utilities.py b/b_asic/utilities.py deleted file mode 100644 index 25707ff8..00000000 --- a/b_asic/utilities.py +++ /dev/null @@ -1,21 +0,0 @@ -"""@package docstring -B-ASIC Operation Module. -TODO: More info. -""" - -from typing import Set -from collections import deque - -from b_asic.operation import Operation - -def breadth_first_search(start: Operation) -> Operation: - """Use breadth first search to traverse the operation tree.""" - visited: Set[Operation] = {start} - queue = deque([start]) - while queue: - operation = queue.popleft() - yield operation - for n_operation in operation.neighbours: - if n_operation not in visited: - visited.add(n_operation) - queue.append(n_operation) diff --git a/test/basic_operations/test_basic_operations.py b/test/core_operations/test_core_operations.py similarity index 94% rename from test/basic_operations/test_basic_operations.py rename to test/core_operations/test_core_operations.py index 21561074..1d33bfe1 100644 --- a/test/basic_operations/test_basic_operations.py +++ b/test/core_operations/test_core_operations.py @@ -1,12 +1,12 @@ """ -B-ASIC test suite for the basic operations. +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 from b_asic.signal import Signal import pytest -""" Constant tests. """ +# Constant tests. def test_constant(): constant_operation = Constant(3) assert constant_operation.evaluate() == 3 @@ -19,7 +19,7 @@ def test_constant_complex(): constant_operation = Constant(3+4j) assert constant_operation.evaluate() == 3+4j -""" Addition tests. """ +# Addition tests. def test_addition(): test_operation = Addition() constant_operation = Constant(3) @@ -38,7 +38,7 @@ def test_addition_complex(): constant_operation_2 = Constant((4+6j)) assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == (7+11j) -""" Subtraction tests. """ +# Subtraction tests. def test_subtraction(): test_operation = Subtraction() constant_operation = Constant(5) @@ -57,7 +57,7 @@ def test_subtraction_complex(): constant_operation_2 = Constant((4+6j)) assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == (-1-1j) -""" Multiplication tests. """ +# Multiplication tests. def test_multiplication(): test_operation = Multiplication() constant_operation = Constant(5) @@ -76,7 +76,7 @@ def test_multiplication_complex(): constant_operation_2 = Constant((4+6j)) assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == (-18+38j) -""" Division tests. """ +# Division tests. def test_division(): test_operation = Division() constant_operation = Constant(30) @@ -95,7 +95,7 @@ def test_division_complex(): constant_operation_2 = Constant((10+20j)) assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == (2.8-1.6j) -""" SquareRoot tests. """ +# SquareRoot tests. def test_squareroot(): test_operation = SquareRoot() constant_operation = Constant(36) @@ -111,7 +111,7 @@ def test_squareroot_complex(): constant_operation = Constant((48+64j)) assert test_operation.evaluate(constant_operation.evaluate()) == (8+4j) -""" ComplexConjugate tests. """ +# ComplexConjugate tests. def test_complexconjugate(): test_operation = ComplexConjugate() constant_operation = Constant(3+4j) @@ -122,7 +122,7 @@ def test_test_complexconjugate_negative(): constant_operation = Constant(-3-4j) assert test_operation.evaluate(constant_operation.evaluate()) == (-3+4j) -""" Max tests. """ +# Max tests. def test_max(): test_operation = Max() constant_operation = Constant(30) @@ -135,7 +135,7 @@ def test_max_negative(): constant_operation_2 = Constant(-5) assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == -5 -""" Min tests. """ +# Min tests. def test_min(): test_operation = Min() constant_operation = Constant(30) @@ -148,7 +148,7 @@ def test_min_negative(): constant_operation_2 = Constant(-5) assert test_operation.evaluate(constant_operation.evaluate(), constant_operation_2.evaluate()) == -30 -""" Absolute tests. """ +# Absolute tests. def test_absolute(): test_operation = Absolute() constant_operation = Constant(30) @@ -164,7 +164,7 @@ def test_absolute_complex(): constant_operation = Constant((3+4j)) assert test_operation.evaluate(constant_operation.evaluate()) == 5.0 -""" ConstantMultiplication tests. """ +# ConstantMultiplication tests. def test_constantmultiplication(): test_operation = ConstantMultiplication(5) constant_operation = Constant(20) @@ -180,7 +180,7 @@ def test_constantmultiplication_complex(): constant_operation = Constant((3+4j)) assert test_operation.evaluate(constant_operation.evaluate()) == (1+18j) -""" ConstantAddition tests. """ +# ConstantAddition tests. def test_constantaddition(): test_operation = ConstantAddition(5) constant_operation = Constant(20) @@ -196,7 +196,7 @@ def test_constantaddition_complex(): constant_operation = Constant((3+4j)) assert test_operation.evaluate(constant_operation.evaluate()) == (6+6j) -""" ConstantSubtraction tests. """ +# ConstantSubtraction tests. def test_constantsubtraction(): test_operation = ConstantSubtraction(5) constant_operation = Constant(20) @@ -212,7 +212,7 @@ def test_constantsubtraction_complex(): constant_operation = Constant((3+4j)) assert test_operation.evaluate(constant_operation.evaluate()) == (-1-2j) -""" ConstantDivision tests. """ +# ConstantDivision tests. def test_constantdivision(): test_operation = ConstantDivision(5) constant_operation = Constant(20) @@ -226,4 +226,4 @@ def test_constantdivision_negative(): def test_constantdivision_complex(): test_operation = ConstantDivision(2+2j) constant_operation = Constant((10+10j)) - assert test_operation.evaluate(constant_operation.evaluate()) == (5+0j) \ No newline at end of file + assert test_operation.evaluate(constant_operation.evaluate()) == (5+0j) diff --git a/test/fixtures/operation_tree.py b/test/fixtures/operation_tree.py index b97e89d7..74d3b8c6 100644 --- a/test/fixtures/operation_tree.py +++ b/test/fixtures/operation_tree.py @@ -10,9 +10,9 @@ def operation(): def create_operation(_type, dest_oper, index, **kwargs): oper = _type(**kwargs) oper_signal = Signal() - oper._output_ports[0].connect_signal(oper_signal) + oper._output_ports[0].add_signal(oper_signal) - dest_oper._input_ports[index].connect_signal(oper_signal) + dest_oper._input_ports[index].add_signal(oper_signal) return oper @pytest.fixture @@ -51,10 +51,10 @@ def large_operation_tree(): add_oper_3 = Addition() add_oper_signal = Signal(add_oper.output(0), add_oper_3.output(0)) - add_oper._output_ports[0].connect_signal(add_oper_signal) - add_oper_3._input_ports[0].connect_signal(add_oper_signal) + add_oper._output_ports[0].add_signal(add_oper_signal) + add_oper_3._input_ports[0].add_signal(add_oper_signal) add_oper_2_signal = Signal(add_oper_2.output(0), add_oper_3.output(0)) - add_oper_2._output_ports[0].connect_signal(add_oper_2_signal) - add_oper_3._input_ports[1].connect_signal(add_oper_2_signal) + 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 diff --git a/test/port/test_inputport.py b/test/port/test_inputport.py index 7a78d5f7..a4324069 100644 --- a/test/port/test_inputport.py +++ b/test/port/test_inputport.py @@ -39,9 +39,9 @@ def connected_sig(): inp_port = InputPort(0, None) return Signal(source=out_port, destination=inp_port) -def test_connect_port_then_disconnect(inp_port, out_port): +def test_connect_then_disconnect(inp_port, out_port): """Test connect unused port to port.""" - s1 = inp_port.connect_port(out_port) + s1 = inp_port.connect(out_port) assert inp_port.connected_ports == [out_port] assert out_port.connected_ports == [inp_port] @@ -50,7 +50,7 @@ def test_connect_port_then_disconnect(inp_port, out_port): assert s1.source is out_port assert s1.destination is inp_port - inp_port.disconnect_signal() + inp_port.remove_signal(s1) assert inp_port.connected_ports == [] assert out_port.connected_ports == [] @@ -61,12 +61,13 @@ def test_connect_port_then_disconnect(inp_port, out_port): 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_port(out_port) + inp_port.connect(out_port) with pytest.raises(AssertionError): - inp_port.connect_port(out_port2) + inp_port.connect(out_port2) -def test_connect_signal_then_disconnect(inp_port, s_w_source): - inp_port.connect_signal(s_w_source) +def test_add_signal_then_disconnect(inp_port, s_w_source): + """Can signal be connected then disconnected properly?""" + inp_port.add_signal(s_w_source) assert inp_port.connected_ports == [s_w_source.source] assert s_w_source.source.connected_ports == [inp_port] @@ -74,7 +75,7 @@ def test_connect_signal_then_disconnect(inp_port, s_w_source): assert s_w_source.source.signals == [s_w_source] assert s_w_source.destination is inp_port - inp_port.disconnect_signal() + inp_port.remove_signal(s_w_source) assert inp_port.connected_ports == [] assert s_w_source.source.connected_ports == [] @@ -82,3 +83,13 @@ def test_connect_signal_then_disconnect(inp_port, s_w_source): 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/port/test_outputport.py b/test/port/test_outputport.py index f48afbdb..ac50818e 100644 --- a/test/port/test_outputport.py +++ b/test/port/test_outputport.py @@ -14,18 +14,33 @@ def test_connect_multiple_signals(inp_ports): out_port = OutputPort(0, None) for port in inp_ports: - out_port.connect_port(port) - + out_port.connect(port) + assert out_port.signal_count() == len(inp_ports) def test_disconnect_multiple_signals(inp_ports): - """Can multiple ports disconnect from an output port?""" + """Can multiple signals disconnect from an output port?""" + out_port = OutputPort(0, None) + + sigs = [] + + for port in inp_ports: + sigs.append(out_port.connect(port)) + + for sig in sigs: + out_port.remove_signal(sig) + + assert out_port.signal_count() == 0 + +def test_disconnect_mulitple_ports(inp_ports): + """Can multiple ports be disconnected from an output port?""" out_port = OutputPort(0, None) for port in inp_ports: - out_port.connect_port(port) - - for _ in inp_ports: - out_port.disconnect_signal(0) + out_port.connect(port) + + for port in inp_ports: + out_port.disconnect(port) - assert out_port.signal_count() == 0 \ No newline at end of file + assert out_port.signal_count() == 3 + assert len(out_port.connected_ports) == 0 \ No newline at end of file diff --git a/test/signal/test_signal.py b/test/signal/test_signal.py index 8c10d1e3..ab07eb77 100644 --- a/test/signal/test_signal.py +++ b/test/signal/test_signal.py @@ -18,7 +18,7 @@ def test_signal_creation_and_disconnction_and_connection_changing(): assert s.destination is in_port in_port1 = InputPort(0, None) - s.connect_destination(in_port1) + s.set_destination(in_port1) assert in_port.signals == [] assert in_port1.signals == [s] @@ -26,14 +26,14 @@ def test_signal_creation_and_disconnction_and_connection_changing(): assert s.source is out_port assert s.destination is in_port1 - s.disconnect_source() + s.remove_source() assert out_port.signals == [] assert in_port1.signals == [s] assert s.source is None assert s.destination is in_port1 - s.disconnect_destination() + s.remove_destination() assert out_port.signals == [] assert in_port1.signals == [] @@ -41,20 +41,20 @@ def test_signal_creation_and_disconnction_and_connection_changing(): assert s.destination is None out_port1 = OutputPort(0, None) - s.connect_source(out_port1) + s.set_source(out_port1) assert out_port1.signals == [s] assert s.source is out_port1 assert s.destination is None - s.connect_source(out_port) + s.set_source(out_port) assert out_port.signals == [s] assert out_port1.signals == [] assert s.source is out_port assert s.destination is None - s.connect_destination(in_port) + s.set_destination(in_port) assert out_port.signals == [s] assert in_port.signals == [s] diff --git a/test/traverse/test_traverse_tree.py b/test/traverse/test_traverse_tree.py index 9f509287..031aeec7 100644 --- a/test/traverse/test_traverse_tree.py +++ b/test/traverse/test_traverse_tree.py @@ -24,7 +24,7 @@ def test_traverse_type(large_operation_tree): def test_traverse_loop(operation_tree): add_oper_signal = Signal() - operation_tree._output_ports[0].connect_signal(add_oper_signal) - operation_tree._input_ports[0].disconnect_signal() - operation_tree._input_ports[0].connect_signal(add_oper_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 -- GitLab From 3dbb4ab92c58d11d205813ef6c5b0397352904ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20H=C3=A4rnqvist?= <ivaha717@student.liu.se> Date: Fri, 20 Mar 2020 18:33:15 +0100 Subject: [PATCH 48/50] Misc. fixes --- b_asic/core_operations.py | 54 +++++++++++++++++++-------------------- b_asic/operation.py | 16 ++++++------ 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/b_asic/core_operations.py b/b_asic/core_operations.py index ce1019f3..d06cbab3 100644 --- a/b_asic/core_operations.py +++ b/b_asic/core_operations.py @@ -35,7 +35,7 @@ class Constant(AbstractOperation): self._output_ports = [OutputPort(0, self)] self._parameters["value"] = value - def evaluate(self) -> Any: + def evaluate(self): return self.param("value") @property @@ -59,7 +59,7 @@ class Addition(AbstractOperation): if source2 is not None: self._input_ports[1].connect_to_port(source2) - def evaluate(self, a, b) -> Any: + def evaluate(self, a, b): return a + b @property @@ -82,11 +82,11 @@ class Subtraction(AbstractOperation): if source2 is not None: self._input_ports[1].connect_to_port(source2) - def evaluate(self, a, b) -> Any: + def evaluate(self, a, b): return a - b @property - def type_name(self) -> GraphIDType: + def type_name(self) -> TypeName: return "sub" @@ -105,11 +105,11 @@ class Multiplication(AbstractOperation): if source2 is not None: self._input_ports[1].connect_to_port(source2) - def evaluate(self, a, b) -> Any: + def evaluate(self, a, b): return a * b @property - def type_name(self) -> GraphIDType: + def type_name(self) -> TypeName: return "mul" @@ -128,11 +128,11 @@ class Division(AbstractOperation): if source2 is not None: self._input_ports[1].connect_to_port(source2) - def evaluate(self, a, b) -> Any: + def evaluate(self, a, b): return a / b @property - def type_name(self) -> GraphIDType: + def type_name(self) -> TypeName: return "div" @@ -150,11 +150,11 @@ class SquareRoot(AbstractOperation): self._input_ports[0].connect_to_port(source1) - def evaluate(self, a) -> Any: + def evaluate(self, a): return sqrt((complex)(a)) @property - def type_name(self) -> GraphIDType: + def type_name(self) -> TypeName: return "sqrt" @@ -172,11 +172,11 @@ class ComplexConjugate(AbstractOperation): self._input_ports[0].connect_to_port(source1) - def evaluate(self, a) -> Any: + def evaluate(self, a): return conjugate(a) @property - def type_name(self) -> GraphIDType: + def type_name(self) -> TypeName: return "conj" @@ -195,13 +195,13 @@ class Max(AbstractOperation): if source2 is not None: self._input_ports[1].connect_to_port(source2) - def evaluate(self, a, b) -> Any: + def evaluate(self, a, b): assert not isinstance(a, complex) and not isinstance(b, complex), \ - ("core_operation.Max does not support complex numbers.") + ("core_operations.Max does not support complex numbers.") return a if a > b else b @property - def type_name(self) -> GraphIDType: + def type_name(self) -> TypeName: return "max" @@ -220,13 +220,13 @@ class Min(AbstractOperation): if source2 is not None: self._input_ports[1].connect_to_port(source2) - def evaluate(self, a, b) -> Any: + def evaluate(self, a, b): assert not isinstance(a, complex) and not isinstance(b, complex), \ - ("core_operation.Min does not support complex numbers.") + ("core_operations.Min does not support complex numbers.") return a if a < b else b @property - def type_name(self) -> GraphIDType: + def type_name(self) -> TypeName: return "min" @@ -244,11 +244,11 @@ class Absolute(AbstractOperation): self._input_ports[0].connect_to_port(source1) - def evaluate(self, a) -> Any: + def evaluate(self, a): return np_abs(a) @property - def type_name(self) -> GraphIDType: + def type_name(self) -> TypeName: return "abs" @@ -266,7 +266,7 @@ class ConstantMultiplication(AbstractOperation): if source1 is not None: self._input_ports[0].connect_to_port(source1) - def evaluate(self, a) -> Any: + def evaluate(self, a): return a * self.param("coefficient") @property @@ -288,11 +288,11 @@ class ConstantAddition(AbstractOperation): if source1 is not None: self._input_ports[0].connect_to_port(source1) - def evaluate(self, a) -> Any: + def evaluate(self, a): return a + self.param("coefficient") @property - def type_name(self) -> GraphIDType: + def type_name(self) -> TypeName: return "cadd" @@ -310,11 +310,11 @@ class ConstantSubtraction(AbstractOperation): if source1 is not None: self._input_ports[0].connect_to_port(source1) - def evaluate(self, a) -> Any: + def evaluate(self, a): return a - self.param("coefficient") @property - def type_name(self) -> GraphIDType: + def type_name(self) -> TypeName: return "csub" @@ -332,9 +332,9 @@ class ConstantDivision(AbstractOperation): if source1 is not None: self._input_ports[0].connect_to_port(source1) - def evaluate(self, a) -> Any: + def evaluate(self, a): return a / self.param("coefficient") @property - def type_name(self) -> GraphIDType: + def type_name(self) -> TypeName: return "cdiv" diff --git a/b_asic/operation.py b/b_asic/operation.py index 75644b73..fc007ffd 100644 --- a/b_asic/operation.py +++ b/b_asic/operation.py @@ -86,9 +86,9 @@ class Operation(GraphComponent): @property @abstractmethod - def neighbours(self) -> "List[Operation]": + def neighbors(self) -> "List[Operation]": """Return all operations that are connected by signals to this operation. - If no neighbours are found this returns an empty list + If no neighbors are found, this returns an empty list. """ raise NotImplementedError @@ -175,17 +175,17 @@ class AbstractOperation(Operation, AbstractGraphComponent): return [self] @property - def neighbours(self) -> List[Operation]: - neighbours: List[Operation] = [] + def neighbors(self) -> List[Operation]: + neighbors: List[Operation] = [] for port in self._input_ports: for signal in port.signals: - neighbours.append(signal.source.operation) + neighbors.append(signal.source.operation) for port in self._output_ports: for signal in port.signals: - neighbours.append(signal.destination.operation) + neighbors.append(signal.destination.operation) - return neighbours + return neighbors def traverse(self) -> Operation: """Traverse the operation tree and return a generator with start point in the operation.""" @@ -198,7 +198,7 @@ class AbstractOperation(Operation, AbstractGraphComponent): while queue: operation = queue.popleft() yield operation - for n_operation in operation.neighbours: + for n_operation in operation.neighbors: if n_operation not in visited: visited.add(n_operation) queue.append(n_operation) -- GitLab From 1b995d1924e726e0a30fedc8516609f629243c06 Mon Sep 17 00:00:00 2001 From: Angus Lothian <anglo547@student.liu.se> Date: Tue, 31 Mar 2020 14:28:05 +0200 Subject: [PATCH 49/50] Change from import to import as import for Signal to see if that works since no circular dependencies are involved there. --- b_asic/core_operations.py | 41 ++++++------ b_asic/operation.py | 78 +++++++++++++++++++++-- test/operation/test_abstract_operation.py | 77 ++++++++++++++++++++++ 3 files changed, 167 insertions(+), 29 deletions(-) create mode 100644 test/operation/test_abstract_operation.py diff --git a/b_asic/core_operations.py b/b_asic/core_operations.py index d06cbab3..8902b169 100644 --- a/b_asic/core_operations.py +++ b/b_asic/core_operations.py @@ -55,9 +55,9 @@ class Addition(AbstractOperation): self._output_ports = [OutputPort(0, self)] if source1 is not None: - self._input_ports[0].connect_to_port(source1) + self._input_ports[0].connect(source1) if source2 is not None: - self._input_ports[1].connect_to_port(source2) + self._input_ports[1].connect(source2) def evaluate(self, a, b): return a + b @@ -78,9 +78,9 @@ class Subtraction(AbstractOperation): self._output_ports = [OutputPort(0, self)] if source1 is not None: - self._input_ports[0].connect_to_port(source1) + self._input_ports[0].connect(source1) if source2 is not None: - self._input_ports[1].connect_to_port(source2) + self._input_ports[1].connect(source2) def evaluate(self, a, b): return a - b @@ -101,9 +101,9 @@ class Multiplication(AbstractOperation): self._output_ports = [OutputPort(0, self)] if source1 is not None: - self._input_ports[0].connect_to_port(source1) + self._input_ports[0].connect(source1) if source2 is not None: - self._input_ports[1].connect_to_port(source2) + self._input_ports[1].connect(source2) def evaluate(self, a, b): return a * b @@ -124,9 +124,9 @@ class Division(AbstractOperation): self._output_ports = [OutputPort(0, self)] if source1 is not None: - self._input_ports[0].connect_to_port(source1) + self._input_ports[0].connect(source1) if source2 is not None: - self._input_ports[1].connect_to_port(source2) + self._input_ports[1].connect(source2) def evaluate(self, a, b): return a / b @@ -147,8 +147,7 @@ class SquareRoot(AbstractOperation): self._output_ports = [OutputPort(0, self)] if source1 is not None: - self._input_ports[0].connect_to_port(source1) - + self._input_ports[0].connect(source1) def evaluate(self, a): return sqrt((complex)(a)) @@ -169,8 +168,7 @@ class ComplexConjugate(AbstractOperation): self._output_ports = [OutputPort(0, self)] if source1 is not None: - self._input_ports[0].connect_to_port(source1) - + self._input_ports[0].connect(source1) def evaluate(self, a): return conjugate(a) @@ -191,9 +189,9 @@ class Max(AbstractOperation): self._output_ports = [OutputPort(0, self)] if source1 is not None: - self._input_ports[0].connect_to_port(source1) + self._input_ports[0].connect(source1) if source2 is not None: - self._input_ports[1].connect_to_port(source2) + self._input_ports[1].connect(source2) def evaluate(self, a, b): assert not isinstance(a, complex) and not isinstance(b, complex), \ @@ -216,9 +214,9 @@ class Min(AbstractOperation): self._output_ports = [OutputPort(0, self)] if source1 is not None: - self._input_ports[0].connect_to_port(source1) + self._input_ports[0].connect(source1) if source2 is not None: - self._input_ports[1].connect_to_port(source2) + self._input_ports[1].connect(source2) def evaluate(self, a, b): assert not isinstance(a, complex) and not isinstance(b, complex), \ @@ -241,8 +239,7 @@ class Absolute(AbstractOperation): self._output_ports = [OutputPort(0, self)] if source1 is not None: - self._input_ports[0].connect_to_port(source1) - + self._input_ports[0].connect(source1) def evaluate(self, a): return np_abs(a) @@ -264,7 +261,7 @@ class ConstantMultiplication(AbstractOperation): self._parameters["coefficient"] = coefficient if source1 is not None: - self._input_ports[0].connect_to_port(source1) + self._input_ports[0].connect(source1) def evaluate(self, a): return a * self.param("coefficient") @@ -286,7 +283,7 @@ class ConstantAddition(AbstractOperation): self._parameters["coefficient"] = coefficient if source1 is not None: - self._input_ports[0].connect_to_port(source1) + self._input_ports[0].connect(source1) def evaluate(self, a): return a + self.param("coefficient") @@ -308,7 +305,7 @@ class ConstantSubtraction(AbstractOperation): self._parameters["coefficient"] = coefficient if source1 is not None: - self._input_ports[0].connect_to_port(source1) + self._input_ports[0].connect(source1) def evaluate(self, a): return a - self.param("coefficient") @@ -330,7 +327,7 @@ class ConstantDivision(AbstractOperation): self._parameters["coefficient"] = coefficient if source1 is not None: - self._input_ports[0].connect_to_port(source1) + self._input_ports[0].connect(source1) def evaluate(self, a): return a / self.param("coefficient") diff --git a/b_asic/operation.py b/b_asic/operation.py index fc007ffd..5578e3c4 100644 --- a/b_asic/operation.py +++ b/b_asic/operation.py @@ -109,9 +109,10 @@ class AbstractOperation(Operation, AbstractGraphComponent): self._parameters = {} @abstractmethod - def evaluate(self, *inputs) -> Any: # pylint: disable=arguments-differ + 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.""" + list of input values. + """ raise NotImplementedError def inputs(self) -> List["InputPort"]: @@ -139,15 +140,15 @@ class AbstractOperation(Operation, AbstractGraphComponent): return self._parameters.get(name) def set_param(self, name: str, value: Any) -> None: - assert name in self._parameters # TODO: Error message. + 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. + 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] @@ -155,10 +156,12 @@ class AbstractOperation(Operation, AbstractGraphComponent): 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] + input_values[i] = source.operation.evaluate_outputs(state)[ + source.port_index] self_state.output_values = self.evaluate(input_values) - assert len(self_state.output_values) == output_count # TODO: Error message. + # 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(): @@ -202,3 +205,64 @@ class AbstractOperation(Operation, AbstractGraphComponent): 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 + + if isinstance(other, Operation): + return Addition(self.output(0), other.output(0)) + elif isinstance(other, Number): + return ConstantAddition(other, self.output(0)) + else: + raise TypeError("Other type is not an Operation or a Number.") + + 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 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 __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 + + 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 __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. + """ + # Import here to avoid circular imports. + from b_asic.core_operations import Division, ConstantDivision + + 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.") + diff --git a/test/operation/test_abstract_operation.py b/test/operation/test_abstract_operation.py new file mode 100644 index 00000000..626a2dc3 --- /dev/null +++ b/test/operation/test_abstract_operation.py @@ -0,0 +1,77 @@ +""" +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 + -- GitLab From 7c87314374ef9cb3df6af0bfd6cc9a4abd5356ff Mon Sep 17 00:00:00 2001 From: Kevin Scott <kevsc634@student.liu.se> Date: Wed, 1 Apr 2020 09:51:29 +0200 Subject: [PATCH 50/50] Changed test layout and refactorized some tests for OutputPort --- test/conftest.py | 5 +- test/fixtures/operation_tree.py | 26 +++--- test/fixtures/port.py | 10 +++ test/graph_id/test_graph_id_generator.py | 29 ------- test/port/test_outputport.py | 46 ----------- .../test_signal_flow_graph.py | 10 --- .../test_core_operations.py | 2 - test/test_graph_id_generator.py | 28 +++++++ test/{port => }/test_inputport.py | 0 test/test_operation.py | 31 +++++++ test/test_outputport.py | 80 +++++++++++++++++++ test/{signal => }/test_signal.py | 0 test/traverse/test_traverse_tree.py | 30 ------- 13 files changed, 164 insertions(+), 133 deletions(-) create mode 100644 test/fixtures/port.py delete mode 100644 test/graph_id/test_graph_id_generator.py delete mode 100644 test/port/test_outputport.py delete mode 100644 test/signal_flow_graph/test_signal_flow_graph.py rename test/{core_operations => }/test_core_operations.py (99%) create mode 100644 test/test_graph_id_generator.py rename test/{port => }/test_inputport.py (100%) create mode 100644 test/test_operation.py create mode 100644 test/test_outputport.py rename test/{signal => }/test_signal.py (100%) delete mode 100644 test/traverse/test_traverse_tree.py diff --git a/test/conftest.py b/test/conftest.py index 66ee9630..64f39843 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,3 +1,4 @@ +from test.fixtures.signal import signal, signals +from test.fixtures.operation_tree import * +from test.fixtures.port import * import pytest -from test.fixtures.signal import * -from test.fixtures.operation_tree import * \ No newline at end of file diff --git a/test/fixtures/operation_tree.py b/test/fixtures/operation_tree.py index 74d3b8c6..df3fcac3 100644 --- a/test/fixtures/operation_tree.py +++ b/test/fixtures/operation_tree.py @@ -17,11 +17,10 @@ def create_operation(_type, dest_oper, index, **kwargs): @pytest.fixture def operation_tree(): - """ - Return a addition operation connected with 2 constants. - >---C---+ - ---A - >---C---+ + """Return a addition operation connected with 2 constants. + ---C---+ + ---A + ---C---+ """ add_oper = Addition() create_operation(Constant, add_oper, 0, value=2) @@ -30,15 +29,14 @@ def operation_tree(): @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---+ + """Return a constant operation connected with a large operation tree with 3 other constants and 3 additions. + ---C---+ + ---A---+ + ---C---+ | + +---A + ---C---+ | + ---A---+ + ---C---+ """ add_oper = Addition() add_oper_2 = Addition() diff --git a/test/fixtures/port.py b/test/fixtures/port.py new file mode 100644 index 00000000..4019b3a2 --- /dev/null +++ b/test/fixtures/port.py @@ -0,0 +1,10 @@ +import pytest +from b_asic.port import InputPort, OutputPort + +@pytest.fixture +def input_port(): + return InputPort(0, None) + +@pytest.fixture +def output_port(): + return OutputPort(0, None) diff --git a/test/graph_id/test_graph_id_generator.py b/test/graph_id/test_graph_id_generator.py deleted file mode 100644 index 85fb088d..00000000 --- a/test/graph_id/test_graph_id_generator.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -B-ASIC test suite for graph id generator. -""" - -from b_asic.graph_id import GraphIDGenerator, GraphID - -import pytest - -@pytest.fixture -def graph_id_generator(): - return GraphIDGenerator() - -def test_empty_string_generator(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" - - -def test_normal_string_generator(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" - -def test_different_strings_generator(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" diff --git a/test/port/test_outputport.py b/test/port/test_outputport.py deleted file mode 100644 index ac50818e..00000000 --- a/test/port/test_outputport.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -B-ASIC test suite for OutputPort -TODO: More info -""" -from b_asic import InputPort, OutputPort -import pytest - -@pytest.fixture -def inp_ports(): - return [InputPort(_, None) for _ in range(0,3)] - -def test_connect_multiple_signals(inp_ports): - """Can multiple ports connect to an output port?""" - out_port = OutputPort(0, None) - - for port in inp_ports: - out_port.connect(port) - - assert out_port.signal_count() == len(inp_ports) - -def test_disconnect_multiple_signals(inp_ports): - """Can multiple signals disconnect from an output port?""" - out_port = OutputPort(0, None) - - sigs = [] - - for port in inp_ports: - sigs.append(out_port.connect(port)) - - for sig in sigs: - out_port.remove_signal(sig) - - assert out_port.signal_count() == 0 - -def test_disconnect_mulitple_ports(inp_ports): - """Can multiple ports be disconnected from an output port?""" - out_port = OutputPort(0, None) - - for port in inp_ports: - out_port.connect(port) - - for port in inp_ports: - out_port.disconnect(port) - - assert out_port.signal_count() == 3 - assert len(out_port.connected_ports) == 0 \ No newline at end of file diff --git a/test/signal_flow_graph/test_signal_flow_graph.py b/test/signal_flow_graph/test_signal_flow_graph.py deleted file mode 100644 index d18d2da5..00000000 --- a/test/signal_flow_graph/test_signal_flow_graph.py +++ /dev/null @@ -1,10 +0,0 @@ -from b_asic.signal_flow_graph import SFG -from b_asic.core_operations import Addition, Constant -from b_asic.signal import Signal -from b_asic.signal_flow_graph import SFG - -import pytest - -def test_adding_to_sfg(): - pass - diff --git a/test/core_operations/test_core_operations.py b/test/test_core_operations.py similarity index 99% rename from test/core_operations/test_core_operations.py rename to test/test_core_operations.py index 1d33bfe1..b176b2a6 100644 --- a/test/core_operations/test_core_operations.py +++ b/test/test_core_operations.py @@ -3,8 +3,6 @@ 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 -from b_asic.signal import Signal -import pytest # Constant tests. def test_constant(): diff --git a/test/test_graph_id_generator.py b/test/test_graph_id_generator.py new file mode 100644 index 00000000..b14597ea --- /dev/null +++ b/test/test_graph_id_generator.py @@ -0,0 +1,28 @@ +""" +B-ASIC test suite for graph id generator. +""" + +from b_asic.graph_id import GraphIDGenerator, GraphID +import pytest + +@pytest.fixture +def graph_id_generator(): + return GraphIDGenerator() + +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" + + 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" + + 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" diff --git a/test/port/test_inputport.py b/test/test_inputport.py similarity index 100% rename from test/port/test_inputport.py rename to test/test_inputport.py diff --git a/test/test_operation.py b/test/test_operation.py new file mode 100644 index 00000000..6c37e30b --- /dev/null +++ b/test/test_operation.py @@ -0,0 +1,31 @@ +from b_asic.core_operations import Constant, Addition +from b_asic.signal import Signal +from b_asic.port import InputPort, OutputPort + +import pytest + +class TestTraverse: + def test_traverse_single_tree(self, operation): + """Traverse a tree consisting of one operation.""" + constant = Constant(None) + assert list(constant.traverse()) == [constant] + + def test_traverse_tree(self, operation_tree): + """Traverse a basic addition tree with two constants.""" + assert len(list(operation_tree.traverse())) == 3 + + def test_traverse_large_tree(self, large_operation_tree): + """Traverse a larger tree.""" + assert len(list(large_operation_tree.traverse())) == 7 + + 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 diff --git a/test/test_outputport.py b/test/test_outputport.py new file mode 100644 index 00000000..deed7a1e --- /dev/null +++ b/test/test_outputport.py @@ -0,0 +1,80 @@ +""" +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)] + +class TestConnect: + def test_multiple_ports(self, output_port, list_of_input_ports): + """Can multiple ports connect to an output port?""" + for port in list_of_input_ports: + output_port.connect(port) + + assert output_port.signal_count() == len(list_of_input_ports) + + def test_same_port(self, output_port, list_of_input_ports): + """Check error handing.""" + output_port.connect(list_of_input_ports[0]) + with pytest.raises(AssertionError): + output_port.connect(list_of_input_ports[0]) + + assert output_port.signal_count() == 2 + +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.connected_ports == [s.destination] + +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) + + for port in list_of_input_ports: + output_port.disconnect(port) + + assert output_port.signal_count() == 3 + assert output_port.connected_ports == [] + +class TestRemoveSignal: + def test_one_signal(self, output_port, input_port): + s = output_port.connect(input_port) + output_port.remove_signal(s) + + 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)) + + for sig in sigs: + output_port.remove_signal(sig) + + assert output_port.signal_count() == 0 + assert output_port.signals == [] diff --git a/test/signal/test_signal.py b/test/test_signal.py similarity index 100% rename from test/signal/test_signal.py rename to test/test_signal.py diff --git a/test/traverse/test_traverse_tree.py b/test/traverse/test_traverse_tree.py deleted file mode 100644 index 031aeec7..00000000 --- a/test/traverse/test_traverse_tree.py +++ /dev/null @@ -1,30 +0,0 @@ -from b_asic.core_operations import Constant, Addition -from b_asic.signal import Signal -from b_asic.port import InputPort, OutputPort - -import pytest - -def test_traverse_single_tree(operation): - """Traverse a tree consisting of one operation.""" - constant = Constant(None) - assert list(constant.traverse()) == [constant] - -def test_traverse_tree(operation_tree): - """Traverse a basic addition tree with two constants.""" - assert len(list(operation_tree.traverse())) == 3 - -def test_traverse_large_tree(large_operation_tree): - """Traverse a larger tree.""" - assert len(list(large_operation_tree.traverse())) == 7 - -def test_traverse_type(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(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 -- GitLab