Repository: XedaHQ/xeda (language: python)

Issue:
Update rich requirement from <12,>=11.2.0 to >=11.2.0,<13
Updates the requirements on [rich](https://github.com/willmcgugan/rich) to permit the latest version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/willmcgugan/rich/releases">rich's releases</a>.</em></p>
<blockquote>
<h2>Fix for piping on legacy windows plus optimizations</h2>
<h2>[12.0.1] - 2022-03-22</h2>
<h3>Changed</h3>
<ul>
<li>Improve performance of cell_length <a href="https://github-redirect.dependabot.com/Textualize/rich/pull/2061">Textualize/rich#2061</a></li>
<li>Improve performance of chop_cells <a href="https://github-redirect.dependabot.com/Textualize/rich/pull/2077">Textualize/rich#2077</a></li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Fix capturing stdout on legacy Windows <a href="https://github-redirect.dependabot.com/Textualize/rich/pull/2066">Textualize/rich#2066</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/Textualize/rich/blob/master/CHANGELOG.md">rich's changelog</a>.</em></p>
<blockquote>
<h2>[12.0.1] - 2022-03-22</h2>
<h3>Changed</h3>
<ul>
<li>Improve performance of cell_length <a href="https://github-redirect.dependabot.com/Textualize/rich/pull/2061">Textualize/rich#2061</a></li>
<li>Improve performance of chop_cells <a href="https://github-redirect.dependabot.com/Textualize/rich/pull/2077">Textualize/rich#2077</a></li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Fix capturing stdout on legacy Windows <a href="https://github-redirect.dependabot.com/Textualize/rich/pull/2066">Textualize/rich#2066</a></li>
</ul>
<h2>[12.0.0] - 2022-03-10</h2>
<h3>Added</h3>
<ul>
<li>Added options to TimeRemainingColumn to render a compact time format and render elapsed time when a task is
finished. <a href="https://github-redirect.dependabot.com/Textualize/rich/pull/1992">Textualize/rich#1992</a></li>
<li>Added ProgressColumn <code>MofNCompleteColumn</code> to display raw <code>completed/total</code> column (similar to DownloadColumn,
but displays values as ints, does not convert to floats or add bit/bytes units).
<a href="https://github-redirect.dependabot.com/Textualize/rich/pull/1941">Textualize/rich#1941</a></li>
<li>Replace Colorama with win32 renderer <a href="https://github-redirect.dependabot.com/Textualize/rich/pull/1993">Textualize/rich#1993</a></li>
<li>Add support for namedtuples to <code>Pretty</code> <a href="https://github-redirect.dependabot.com/Textualize/rich/pull/2031">Textualize/rich#2031</a></li>
</ul>
<h3>Fixed</h3>
<ul>
<li>In Jupyter mode make the link target be set to &quot;_blank&quot;</li>
<li>Fix some issues with markup handling around &quot;[&quot; characters https://github-redirect.dependabot.com/Textualize/rich/pull/1950</li>
<li>Fix syntax lexer guessing.</li>
<li>Fixed Pretty measure not respecting expand_all <a href="https://github-redirect.dependabot.com/Textualize/rich/issues/1998">Textualize/rich#1998</a></li>
<li>Collapsed definitions for single-character spinners, to save memory and reduce import time.</li>
<li>Fix print_json indent type in <code>__init__.py</code></li>
<li>Fix error when inspecting object defined in REPL <a href="https://github-redirect.dependabot.com/Textualize/rich/pull/2037">Textualize/rich#2037</a></li>
<li>Fix incorrect highlighting of non-indented JSON <a href="https://github-redirect.dependabot.com/Textualize/rich/pull/2038">Textualize/rich#2038</a></li>
<li>Fixed height reset in complex renderables <a href="https://github-redirect.dependabot.com/Textualize/rich/issues/2042">Textualize/rich#2042</a></li>
</ul>
<h3>Changed</h3>
<ul>
<li>Improved support for enum.Flag in ReprHighlighter <a href="https://github-redirect.dependabot.com/Textualize/rich/pull/1920">Textualize/rich#1920</a></li>
<li>Tree now respects justify=None, i.e. won't pad to right <a href="https://github-redirect.dependabot.com/Textualize/rich/issues/1690">Textualize/rich#1690</a></li>
<li>Removed rich.tabulate which was marked for deprecation</li>
<li>Deprecated rich.align.AlignValues in favor of AlignMethod</li>
</ul>
<h2>[11.2.0] - 2022-02-08</h2>
<h3>Added</h3>
<ul>
<li>Add support for US spelling of &quot;gray&quot; in ANSI color names <a href="https://github-redirect.dependabot.com/Textualize/rich/issues/1890">Textualize/rich#1890</a></li>
<li>Added <code>rich.diagnose.report</code> to expose environment debugging logic as function <a href="https://github-redirect.dependabot.com/Textualize/rich/pull/1917">Textualize/rich#1917</a></li>
<li>Added classmethod <code>Progress.get_default_columns()</code> to get the default list of progress bar columns <a href="https://github-redirect.dependabot.com/Textualize/rich/pull/1894">Textualize/rich#1894</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/Textualize/rich/commit/2d3152a230d2911cf4e7d7648cd7153e784301e3"><code>2d3152a</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/willmcgugan/rich/issues/2093">#2093</a> from Textualize/v12.0.1</li>
<li><a href="https://github.com/Textualize/rich/commit/2956b5d9cad7b516f60877f04704b62efc40e324"><code>2956b5d</code></a> changes</li>
<li><a href="https://github.com/Textualize/rich/commit/037da64d83c844bf7bbef12646349ad7c1f96cc5"><code>037da64</code></a> version bump</li>
<li><a href="https://github.com/Textualize/rich/commit/1572d87854765c4159ce590798dc5990537c02a8"><code>1572d87</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/willmcgugan/rich/issues/2089">#2089</a> from Textualize/recursive-rich-cast</li>
<li><a href="https://github.com/Textualize/rich/commit/e23b0bb3da0bf782d394989b47669f7e75e8d98b"><code>e23b0bb</code></a> allow rich cast to return rich cast</li>
<li><a href="https://github.com/Textualize/rich/commit/ef1b9b91ccff680b7f931d75fd92c3caa6fcd622"><code>ef1b9b9</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/willmcgugan/rich/issues/2088">#2088</a> from Textualize/panel-heading-glitch</li>
<li><a href="https://github.com/Textualize/rich/commit/6d443ddaf0ef5cf4e1425b5ff932650dd2692cc0"><code>6d443dd</code></a> panel bug</li>
<li><a href="https://github.com/Textualize/rich/commit/a278b6e34eb789bbf7daa2e01d7daf2f509b7622"><code>a278b6e</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/willmcgugan/rich/issues/2081">#2081</a> from Textualize/win32-refinements</li>
<li><a href="https://github.com/Textualize/rich/commit/1f758c4acb98ace1cd1710e2c70f4ba2fbbffbb3"><code>1f758c4</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/willmcgugan/rich/issues/2082">#2082</a> from Textualize/fix-uuid-highlight</li>
<li><a href="https://github.com/Textualize/rich/commit/0daccb07684760c86a5583ee98bb94a406837992"><code>0daccb0</code></a> fix uuid highlighter</li>
<li>Additional commits viewable in <a href="https://github.com/willmcgugan/rich/compare/v11.2.0...v12.0.1">compare view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Discussion / hints:

Fix patch:
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..f8ffeb47
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,1 @@
+src/xeda/_version.py export-subst
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..91f7a5a5
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,7 @@
+version: 2
+updates:
+- package-ecosystem: pip
+  directory: "/"
+  schedule:
+    interval: daily
+  open-pull-requests-limit: 10
\ No newline at end of file
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 00000000..6f76268b
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,70 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+  push:
+    branches: [ dev ]
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [ dev ]
+  schedule:
+    - cron: '44 19 * * 3'
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: [ 'python' ]
+        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
+        # Learn more about CodeQL language support at https://git.io/codeql-language-support
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v2
+
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v1
+      with:
+        languages: ${{ matrix.language }}
+        # If you wish to specify custom queries, you can do so here or in a config file.
+        # By default, queries listed here will override any specified in a config file.
+        # Prefix the list here with "+" to use these queries and those in the config file.
+        # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
+    # If this step fails, then you should remove it and run the build manually (see below)
+    - name: Autobuild
+      uses: github/codeql-action/autobuild@v1
+
+    # ℹ️ Command-line programs to run using the OS shell.
+    # 📚 https://git.io/JvXDl
+
+    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+    #    and modify them (or add more) to build your code if your project
+    #    uses a compiled language
+
+    #- run: |
+    #   make bootstrap
+    #   make release
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v1
diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml
new file mode 100644
index 00000000..18d1e9cb
--- /dev/null
+++ b/.github/workflows/python-publish.yml
@@ -0,0 +1,23 @@
+name: Upload 📦 to PyPI
+on: push
+jobs:
+  deploy:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+    - name: Set up 🐍
+      uses: actions/setup-python@v3
+      with:
+        python-version: 3.9
+    - name: Install pypa/build
+      run: python -m pip install --upgrade --user build
+    - name: Build a binary wheel and a source tarball
+      run: python -m build --sdist --wheel --outdir dist/
+    - name: Publish 📦 to PyPI
+      if: startsWith(github.ref, 'refs/tags')
+      uses: pypa/gh-action-pypi-publish@release/v1
+      with:
+        user: __token__
+        password: ${{ secrets.PYPI_API_TOKEN }}
+        packages_dir: dist/
+        repository: pypi
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 00000000..b456dbfb
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,10 @@
+version: 2
+
+sphinx:
+  configuration: docs/conf.py
+  fail_on_warning: false
+
+python:
+  version: "3.8"
+  install:
+    - requirements: docs/requirements.txt
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..394787f2
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,25 @@
+# Changelog
+All notable changes to this project will be documented in this file.
+
+
+## [Unreleased]
+
+### Fixed
+- Command line interface:
+    - `list-settings`: improved display of types and default values
+### Changed
+- WIP: Handling settings of dependency flow during `Settings` validation.
+### Removed
+
+### Added
+- Tool: ecppll (nextpnr's Lattice ECP5 PLL tool)
+- Examples: ULX3S adopted DVI test from EMARD
+  - change LED chaser speed with buttons `5` and `6`!
+- Examples: improved version of `blinky` for the ULX3S board in VHDL
+  - change LED chaser speed with buttons `5` and `6`!
+
+## [v0.1.0-alpha.11] - 2022-04-16
+
+
+[Unreleased]: https://github.com/XedaHQ/xeda/compare/v0.1.0-alpha.1...HEAD
+[v0.1.0-alpha.11]: https://github.com/XedaHQ/xeda/releases/tag/v0.1.0-alpha.11
\ No newline at end of file
diff --git a/xeda/data/__init__.py b/MANIFEST.in
similarity index 100%
rename from xeda/data/__init__.py
rename to MANIFEST.in
diff --git a/README.md b/README.md
index 6c09b91d..8dcd9d02 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,9 @@
-[![Documentation Status](https://readthedocs.org/projects/xeda/badge/?version=latest)](https://xeda.readthedocs.io/en/latest/?badge=latest)
 
-![Xeda Logo](logo.svg)
+[![CI](https://github.com/XedaHQ/xeda/workflows/CI/badge.svg)](https://github.com/XedaHQ/xeda/actions?query=event%3Apush+branch%3Adev+workflow%3ACI) [![Downloads](https://static.pepy.tech/personalized-badge/xeda?period=total&units=none&left_color=black&right_color=orange&left_text=Downloads)](https://pepy.tech/project/xeda) [![license](https://img.shields.io/github/license/XedaHQ/xeda)](https://github.com/XedaHQ/xeda/blob/master/LICENSE.txt) [![versions](https://img.shields.io/pypi/pyversions/xeda)](https://pypi.org/project/xeda) [![PyPI](https://img.shields.io/pypi/v/xeda)](https://pypi.org/project/xeda/) [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/XedaHQ/xeda.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/XedaHQ/xeda/context:python)
+
+
+
+[![Xeda Logo](https://raw.githubusercontent.com/XedaHQ/xeda/dev/xeda.png?raw=true)](https://github.com/XedaHQ/xeda)
 
 
 **Xeda** `/ˈziːdə/` is a cross-platform, cross-EDA, cross-target simulation and synthesis automation platform.
@@ -14,15 +17,7 @@ For further details, visit the [Xeda's documentations](http://xeda.rtfd.io/) (Wo
 
 
 ## Installation
-
-Requires Python >= 3.6.9
-
-- Install from GitHub's master branch (recommended during alpha development):
-```
-python3 -m pip install -U git+https://github.com/XedaHQ/xeda.git
-```
-
-- Install the latest published version from [pypi](https://pypi.org/project/xeda):
+Python 3.8 or newer is required. To install the latest published version from [pypi](https://pypi.org/project/xeda) run:
 ```
 python3 -m pip install -U xeda
 ```
@@ -34,84 +29,65 @@ cd xeda
 python3 -m pip install -U -e .
 ```
 
+## Usage
+Run `xeda --help` to see a list of available commands and options.
 
 
-## Xeda Project File
+### Design Description
 
 Xeda design-specific descriptions and settings are organized through project files specified in [TOML](https://toml.io/). Every project contains one or more HDL designs. The default name for the project file is `xedaproject.toml`.
 
-Sample `xedaproject.toml`:
+Sample Xeda design description [file](./examples/vhdl/sqrt/sqrt.toml):
 
 ```toml
-[project]
-name = "Project1"
-description = "My Project with 2 designs"
-
-[[design]]
-name = 'Design1'
-[design.rtl]
-sources = [
-    'src_rtl/module1.vhd',
-    'src_rtl/top.v'
-]
-top = 'Top'
-clock = 'clk'
-[design.tb]
-sources = [
-    'top_tb.vhd',
-]
-top = 'TopTB'
-
-[[design]]
-name = 'Design2'
-[design.rtl]
-sources = [
-    'src_rtl/module2.v',
-    'src_rtl/top2.vhd'
-]
-top = 'Top2'
-clock = 'clk'
-[design.tb]
-sources = [
-    'cocoTestBench.py',
-]
-top = 'cocoTestBench'
-
+name = 'sqrt'
+description = 'Integer Square Root'
+language.vhdl.standard = "2008"
+
+[rtl]
+sources = ['sqrt.vhdl']
+top = 'sqrt'
+clock_port = 'clk'
+parameters = { G_IN_WIDTH = 32 }
+# parameters = { G_IN_WIDTH = 32, G_ITERATIVE = true, G_STR= "abcd", G_BITVECTOR="7'b0101001" }
+
+[tb]
+sources = ['tb/tb_sqrt.py']
+cocotb = true
+top = 'tb_sqrt'
 ```
-Flow- or plugin-specific settings can also be stored in design or project sections.
 
-
-Settings override in the following order, from lower to higher priority:
-- System-wide `default.json` in `<DATA_DIR>/config/xeda/defaults.json`
-- Design-specific settings in the current project file (`xedaproject.toml`)
-- `--override-settings` and `--override-flow-settings` command line options
-
-## Supported Flows
-
-- Xilinx® Vivado® Design Suite
-    - `vivado_synth`: Full synthesis and implementation flow. Supported strategies:
-      - `Timing`, `Timing2`, `Timing3`: Optimize for timing performance
-      - `Area`: Optimize the flow for lowest resource usage
-      - `Runtime`: Quick run of implementation flow
-      - `Debug`: Quick flow keeping details of the design hierarchy, suitable for debugging post-synthesis issues
+## Flows
+A `Tool` is an abstraction for an executable which is responsible for one or several steps in an EDA flow. A `Tool` can be executed as a native binary already installed on the system, in a virtualized container (e.g. `docker`), or on a remote system.
+A `Flow` is a collection of steps performed by one or several tools. A Xeda `Flow` implements the following methods:
+- `init`(optional): initializations which need to happen after  the instantiation of a `Flow` instance. At this stage, the flow can specify and customize dependency flows, which will be run before execution of the flow. Seperation of this stage form Python `__init__` enables greater flexibility and more effective control over the execution of flows.
+- `run`: main execution of the flow which includes generation of files, scripts, and tool arguments as well as execution of one or several tools. All dependencies have been already executed before `run` begins, and the completed dependencies (and their results and artifacts) will be available.
+- `parse_results`(optional): evaluate and interpret generated reports or other artifacts.
+
+### Supported Tools and Flows
+
+- [GHDL](https://github.com/ghdl/ghdl) VHDL simulator
+  - `ghdl_sim`
+- Intel [Quartus Prime](https://www.intel.com/content/www/us/en/software/programmable/quartus-prime/overview.html) (Lite/Pro Editions):
+  - `quartus`: FPGA synthesis and implementation flow
+- Lattice Diamond
+  - `diamond_synth`: FPGA synthesis and implementation flow
+- Mentor (Siemens) [ModelSim](https://eda.sw.siemens.com/en-US/ic/modelsim/)
+  - `modelsim` RTL and netlist simulation
+- [nextpnr](https://github.com/YosysHQ/nextpnr) portable FPGA place and route tool
+- [openFPGAloader](https://github.com/trabucayre/openFPGALoader): Open-source and multi-platform universal utility for programming FPGAs. Compatible with many boards, cables and FPGA from major manufacturers.
+- Synopsys Design Compiler
+- Synopsys VCS simulator
+- AMD-Xilinx [Vivado](https://www.xilinx.com/products/design-tools/vivado/vivado-ml.html) Design Suite
+    - `vivado_synth`: FPGA synthesis and implementation.
     - `vivado_sim`: functional simulation of RTL design
-    - `vivado_postsynth_sim`: Post-implementation functional and timing simulation and power analysis
+    - `vivado_postsynthsim`: Post-implementation functional and timing simulation and power analysis
     - `vivado_power`: Post-implementation power estimation based on post-implementation timing simulation with real-world target testvectors
-- Lattice® Diamond®
-    - synth: Full synthesis and implementation flow
-- Intel® Quartus® Prime Lite/Pro:
-    - synth: Full synthesis and implementation flow
+- AMD-Xilinx [ISE](https://www.xilinx.com/products/design-tools/ise-design-suite.html) Design Suite
+- [Yosys](https://github.com/YosysHQ/yosys) Open SYnthesis Suite
 
-- Synopsys Design Compiler
-- Synopsys VCS simulator
-- Mentor ModelSim
 
-Open Source Tool Support:
-- GHDL
-- Verilator
-- Yosys
-- nextpnr, prjtrellis, and openFPGAloader
+Run `xeda list-flows` for the full list of supported flows in the installed version.
+
 
 
-## Supported Flow Runners
-- `fmax`: determine the maximum frequency of a design through a smart binary search
diff --git a/TODO.md b/TODO.md
index 572f0d9d..0885d6ac 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,33 +1,26 @@
 # Documentation
-- [ ] Everything
-- [ ] tutorials on readthedocs
-- [ ] Developer docs for adding new plugins
+- [ ] Usage
+- [ ] Development
+- [ ] Adding new flows
 - [ ] Developer docs for adding new flows
 
 # Main code
 
-- [ ] FIX ligering child processes after being killed
+- [ ] FIX lingering child processes after being killed
 - [x] Idea: Some code in Suite should be refactored to a FlowRunner class, suites/flows? should provide a run method.
 - [x] parallel runs
-- [ ] Flow chaining
+- [.] Flow chaining
 - [ ] move DSE code to a plugin
 - [ ] Installation / set-up of tools
 - [ ] Run in docker
 
 ## Flows
 
-- [ ] Support multiple clocks in synthesis at least with DefaultRunner
+- [.] Support multiple clocks in synthesis at least with DefaultRunner
 - [x] Modelsim
-- [ ] ghdl + yosys + nextpnr-ecp5
+- [.] ghdl + yosys + nextpnr-ecp5
 - [ ] Synopsys VCS
-- [ ] Synopsys DC
+- [.] Synopsys DC
 - [ ] Synopsys ICC2
 - [ ] OpenROAD/OpenLANE
-- [ ] Quartus: Power estimation
-
-# Plugins
-
-## LWC plugins
-
-- [ ] LwcSim post_run should actually do the `regexp` on stdout and/or check results.txt
-- [ ] LwcSynth post_results: 1. check for usage of forbidden resources 2. only keep relevant data 
\ No newline at end of file
+- [x] Quartus: Power estimation
diff --git a/docs/conf.py b/docs/conf.py
index dab0c9a0..5bd3c258 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -12,30 +12,48 @@
 #
 # import os
 # import sys
-# sys.path.insert(0, os.path.abspath('.'))
+# sys.path.insert(0, os.path.abspath("."))
 
+from importlib.metadata import version as release_version
 
 # -- Project information -----------------------------------------------------
 
-project = 'XEDA'
-copyright = '2020, Kamyar Mohajerani'
-author = 'Kamyar Mohajerani'
+project = "xeda"
+copyright = "2022, Kamyar Mohajerani"
+author = "Kamyar Mohajerani"
+version = release_version("xeda")
+
+master_doc = "index"
+language = "en"
 
 
 # -- General configuration ---------------------------------------------------
 
 # Add any Sphinx extension module names here, as strings. They can be
-# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# extensions coming with Sphinx (named "sphinx.ext.*") or your custom
 # ones.
-extensions = ['recommonmark', 'sphinx_rtd_theme']
+extensions = [
+    # "sphinxcontrib.bibtex",
+    "myst_parser",
+    "sphinx.ext.autodoc",
+    "sphinx.ext.intersphinx",
+    "sphinx.ext.viewcode",
+    "sphinx_panels",
+    "sphinxext.rediraffe",
+    "sphinxcontrib.mermaid",
+    "sphinxext.opengraph",
+    "sphinxcontrib.autodoc_pydantic",
+    "sphinx.ext.autosummary",
+    "sphinx.ext.autodoc"
+]
 
 # Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
 
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
 # This pattern also affects html_static_path and html_extra_path.
-exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
 
 
 # -- Options for HTML output -------------------------------------------------
@@ -43,9 +61,28 @@
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
 #
-html_theme = 'sphinx_rtd_theme'
+html_theme = "sphinx_book_theme"
+html_logo = "../logo.svg"
+# html_favicon = "../logo.svg"
+html_title = ""
+html_theme_options = {
+    "github_url": "https://github.com/XedaHQ/xeda",
+    "repository_url": "https://github.com/XedaHQ/xeda",
+    "use_edit_page_button": True,
+    "repository_branch": "dev",
+    "path_to_docs": "docs",
+}
 
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+html_static_path = ["_static"]
+
+intersphinx_mapping = {
+    "python": ("https://docs.python.org/3.9", None),
+    "pydantic": ("https://www.sphinx-doc.org/en/master", None),
+}
+
+
+autosummary_generate = True
+# autodoc_member_order = "bysource"
diff --git a/docs/flows.rst b/docs/flows.rst
index 2b183284..f96cece2 100644
--- a/docs/flows.rst
+++ b/docs/flows.rst
@@ -4,7 +4,3 @@
 Flows
 -----
 
-Reports Directory
-=================
-
-``reports_subdir_name`` is the name of the default reports subdirectory withint a flow's ``run_dir``. All the tools in a flow save their _report_ files inside this directory. The files in this directory will be parsed during execution of a flow's ``parse_report()``.
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 00000000..82478841
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,12 @@
+sphinx
+docutils
+sphinx-tabs
+sphinx-jinja
+sphinxcontrib.bibtex
+sphinx_panels
+sphinxext.rediraffe
+sphinxcontrib.mermaid
+sphinxext.opengraph
+autodoc_pydantic
+sphinx_book_theme
+myst-parser
\ No newline at end of file
diff --git a/examples/blinky/blinky.v b/examples/blinky/blinky.v
deleted file mode 100644
index 1e008e27..00000000
--- a/examples/blinky/blinky.v
+++ /dev/null
@@ -1,35 +0,0 @@
-
-module top(input  clk_25mhz,
-               input  [6:0] btn,
-               output [7:0] led);
-
-wire clk;
-assign clk = clk_25mhz;
-
-// __GEN_ECP5_PLL gen_pll_inst(.in_clk(clk_25mhz), .out_clk(pll_clk0), .locked(pll_locked));
-
-// top top_inst (
-//         .clk(pll_clk0),
-//         .btn(btn),
-//         .led(led)
-//     );
-
-reg [7:0] o_led;
-assign led = o_led;
-
-localparam ctr_width = 32;
-localparam ctr_lsb = 20;
-reg [ctr_width-1:0] ctr = 0;
-
-wire [2:0] cx;
-wire [2:0] shift_val;
-
-assign cx = ctr[ctr_lsb + 2:ctr_lsb];
-assign shift_val = ctr[ctr_lsb + 3] ? (7 - cx) : cx;
-
-always @(posedge clk) begin
-    ctr <= ctr + 1;
-    o_led <= 1 << shift_val;
-end
-
-endmodule
diff --git a/examples/blinky/xedaproject.toml b/examples/blinky/xedaproject.toml
deleted file mode 100644
index 39e3093b..00000000
--- a/examples/blinky/xedaproject.toml
+++ /dev/null
@@ -1,11 +0,0 @@
-[[design]]
-name = 'blinky'
-description = 'Running LEDs on the ULX3S board'
-
-[design.rtl]
-sources = ['blinky.v']
-top = 'top'
-
-[flows.open_fpga_loader]
-board = 'ulx3s'
-clock_period = 20 # 50 MHz
diff --git a/examples/boards/ulx3s/blinky/blinky.v b/examples/boards/ulx3s/blinky/blinky.v
new file mode 100644
index 00000000..0f4e00e8
--- /dev/null
+++ b/examples/boards/ulx3s/blinky/blinky.v
@@ -0,0 +1,30 @@
+
+module blinky(
+    input  clk_25mhz,
+    input  [6:0] btn,
+    output [7:0] led
+);
+
+    localparam ctr_width = 32;
+    localparam ctr_lsb = 20;
+
+    wire clk;
+    assign clk = clk_25mhz;
+
+    // __GEN_ECP5_PLL gen_pll_inst(.in_clk(clk_25mhz), .out_clk(pll_clk0), .locked(pll_locked));
+
+
+    reg [ctr_width-1:0] ctr;
+
+    wire [2:0] cx;
+    wire [2:0] shift_val;
+
+    assign cx = ctr[ctr_lsb + 2:ctr_lsb];
+    assign shift_val = ctr[ctr_lsb + 3] ? (7 - cx) : cx;
+
+    always @(posedge clk) begin
+        ctr <= ctr + 1;
+        led <= 1 << shift_val;
+    end
+
+endmodule
diff --git a/examples/boards/ulx3s/blinky/blinky.vhdl b/examples/boards/ulx3s/blinky/blinky.vhdl
new file mode 100644
index 00000000..0d6395c5
--- /dev/null
+++ b/examples/boards/ulx3s/blinky/blinky.vhdl
@@ -0,0 +1,181 @@
+--===============================================================================================--
+--! @file              blinky.vhdl
+--! @brief             
+--! @author            Kamyar Mohajerani
+--! @copyright         Copyright (c) 2022 Kamyar Mohajerani
+--!
+--! @license           Solderpad Hardware License v2.1 ([SHL-2.1](https://solderpad.org/licenses/SHL-2.1/))
+--! @vhdl              VHDL 2008 and later
+--!
+--! @details           
+--!
+--===============================================================================================--
+
+library ieee;
+use ieee.std_logic_1164.all;
+use ieee.numeric_std.all;
+
+package utils_pkg is
+    function log2ceil(n : natural) return natural;
+    type T_UNSIGNED_ARRAY is array (natural range <>) of unsigned;
+end package;
+
+package body utils_pkg is
+    --====================================== Functions
+    --! Returns the number of bits required to represet values less than n (0 to n - 1 inclusive)
+    function log2ceil(n : natural) return natural is
+        variable r : natural := 0;
+    begin
+        while n > 2 ** r loop
+            r := r + 1;
+        end loop;
+        return r;
+    end function;
+
+end package body;
+
+library ieee;
+use ieee.std_logic_1164.all;
+use ieee.numeric_std.all;
+
+use work.utils_pkg.all;
+
+entity debouncer is
+    generic(
+        G_W        : positive;
+        G_N_FLOPS  : positive := 2;
+        G_CTR_BITS : positive := log2ceil(25_000_000 * 10 / 1000) -- freq(Hz) * stable_time(ms)
+    );
+    port(
+        clk      : in  std_logic;
+        rst      : in  std_logic := '0';
+        in_bits  : in  std_logic_vector(G_W - 1 downto 0);
+        out_bits : out std_logic_vector(G_W - 1 downto 0)
+    );
+end entity debouncer;
+
+architecture RTL of debouncer is
+    -- registers:
+    signal debounce_ctr : T_UNSIGNED_ARRAY(0 to G_W - 1)(G_CTR_BITS - 1 downto 0);
+    signal flops        : T_UNSIGNED_ARRAY(0 to G_W - 1)(G_N_FLOPS - 1 downto 0) := (others => (others => '0')); -- init for FPGA impl
+    signal out_bits_reg : std_logic_vector(G_W - 1 downto 0);
+begin
+    out_bits <= out_bits_reg;
+
+    process(clk) is
+    begin
+        if rising_edge(clk) then
+            if rst then
+                flops        <= (others => (others => '0'));
+                out_bits_reg <= (others => '0');
+            else
+                for i in 0 to G_W - 1 loop
+                    flops(i) <= in_bits(i) & flops(i)(G_N_FLOPS - 1 downto 1);
+                    if (or flops(i)) and (nand flops(i)) then -- flops content are different (bounce)
+                        debounce_ctr(i) <= (others => '0');
+                    elsif debounce_ctr(i)(G_CTR_BITS - 1) = '0' then -- stable and not enough time
+                        debounce_ctr(i) <= debounce_ctr(i) + 1;
+                    else
+                        out_bits_reg(i) <= flops(i)(0); -- stable and held long enough
+                    end if;
+                end loop;
+            end if;
+        end if;
+    end process;
+end architecture;
+
+library ieee;
+use ieee.std_logic_1164.all;
+use ieee.numeric_std.all;
+
+use work.utils_pkg.log2ceil;
+
+entity blinky is
+    port(
+        -- 25MHz clock
+        clk_25mhz : in    std_logic;
+        -- Buttons
+        btn       : in    std_logic_vector(6 downto 0);
+        -- LEDs
+        led       : out   std_logic_vector(7 downto 0);
+        -- GPIO
+        gp        : inout std_logic_vector(27 downto 0) := (others => 'Z');
+        gn        : inout std_logic_vector(27 downto 0) := (others => 'Z')
+    );
+end entity blinky;
+
+architecture RTL of blinky is
+
+    --====================================== Constants
+    constant MAX_SLOWDOWN          : natural  := 20;
+    constant MIN_SLOWDOWN          : natural  := 14;
+    constant SPEED_CHANGE_SLOWDOWN : natural  := 18;
+    constant LOG2_LED_BITS         : positive := log2ceil(led'length);
+
+    --====================================== Registers
+    signal counter       : unsigned(MAX_SLOWDOWN + LOG2_LED_BITS downto 0);
+    signal speed_counter : unsigned(log2ceil(MAX_SLOWDOWN - MIN_SLOWDOWN) + SPEED_CHANGE_SLOWDOWN - 1 downto 0);
+
+    --====================================== Wires
+    signal clk           : std_logic;
+    signal shift_val     : unsigned(LOG2_LED_BITS - 1 downto 0);
+    signal counter_slice : unsigned(LOG2_LED_BITS downto 0);
+    signal buttons       : std_logic_vector(1 downto 0);
+
+    --====================================== Aliases
+    alias counter_slice_lo : unsigned is counter_slice(LOG2_LED_BITS - 1 downto 0);
+    alias counter_slice_hi : std_logic is counter_slice(LOG2_LED_BITS);
+    alias counter_hi       : unsigned is counter(counter'high downto MIN_SLOWDOWN);
+    alias speed            : unsigned is speed_counter(speed_counter'high downto SPEED_CHANGE_SLOWDOWN);
+begin
+
+    INST_PLL : entity work.ecp5pll
+        generic map(
+            G_IN_HZ    => 25_000_000,
+            G_OUT_0_HZ => 3_125_000,
+            G_RESET_EN => FALSE
+        )
+        port map(
+            clk_i        => clk_25mhz,
+            clk_o_0      => clk,
+            reset        => '0',
+            standby      => '0',
+            phasesel     => (others => '0'),
+            phasedir     => '0',
+            phasestep    => '0',
+            phaseloadreg => '0',
+            locked       => open
+        );
+    -- select a 4 bit slice of `counter`. Lowest bit index is determined by index
+    counter_slice <= shift_right(counter_hi, to_integer(speed))(counter_slice'range);
+    shift_val     <= led'length - 1 - counter_slice_lo when counter_slice_hi else counter_slice_lo;
+
+    INST_DEBOUNCER : entity work.debouncer
+        generic map(
+            G_W        => buttons'length,
+            G_N_FLOPS  => 3,
+            G_CTR_BITS => 14
+        )
+        port map(
+            clk      => clk,
+            in_bits  => (btn(6), btn(5)),
+            out_bits => buttons
+        );
+
+    process(clk) is
+    begin
+        if rising_edge(clk) then
+            counter <= counter + 1;
+            led     <= std_logic_vector(shift_left(to_unsigned(1, led'length), to_integer(shift_val)));
+            if buttons(0) then
+                if nand speed then
+                    speed_counter <= speed_counter + 1; -- saturate high (speed == 11...1)
+                end if;
+            elsif buttons(1) then
+                if or speed then
+                    speed_counter <= speed_counter - 1; -- saturate low (speed == 0)
+                end if;
+            end if;
+        end if;
+    end process;
+end architecture;
diff --git a/examples/boards/ulx3s/blinky/ecp5pll.vhdl b/examples/boards/ulx3s/blinky/ecp5pll.vhdl
new file mode 100644
index 00000000..dac48c94
--- /dev/null
+++ b/examples/boards/ulx3s/blinky/ecp5pll.vhdl
@@ -0,0 +1,353 @@
+
+--===============================================================================================--
+--! @file              ecp5pll.vhdl
+--! @brief             ECP5 PLL wrapper
+--! @author            Kamyar Mohajerani
+--! @copyright         Copyright (c) 2022 Kamyar Mohajerani
+--!
+--! @license           Solderpad Hardware License v2.1 ([SHL-2.1](https://solderpad.org/licenses/SHL-2.1/))
+--! @vhdl              VHDL 2008 and later
+--!
+--! @details          
+--!     - Based on SystemVerilog ecp5pll implementation by EMARD:
+--!         https://github.com/emard/ulx3s-misc/blob/7413e82/examples/ecp5pll/hdl/sv/ecp5pll.sv
+--!     - Actual frequency can be equal or higher than requested
+--===============================================================================================--
+
+library ieee;
+use ieee.std_logic_1164.all;
+use ieee.numeric_std.all;
+
+entity ecp5pll is
+    generic(
+        G_IN_HZ        : positive;
+        G_FREQ_SCALE   : positive := 1;
+        G_OUT_0_HZ     : positive;
+        G_OUT_0_DEG    : natural  := 0; -- keep 0
+        G_OUT_0_TOL_HZ : natural  := 0; -- tolerance: if freq differs more, then error
+        G_OUT_1_HZ     : natural  := 0;
+        G_OUT_1_DEG    : natural  := 0;
+        G_OUT_1_TOL_HZ : natural  := 0;
+        G_OUT_2_HZ     : natural  := 0;
+        G_OUT_2_DEG    : natural  := 0;
+        G_OUT_2_TOL_HZ : natural  := 0;
+        G_OUT_3_HZ     : natural  := 0;
+        G_OUT_3_DEG    : natural  := 0;
+        G_OUT_3_TOL_HZ : natural  := 0;
+        G_RESET_EN     : boolean  := FALSE;
+        G_STANDBY_EN   : boolean  := FALSE;
+        G_DYNAMIC_EN   : boolean  := FALSE
+    );
+    port(
+        clk_i        : in  std_logic;
+        clk_o_0      : out std_logic;
+        clk_o_1      : out std_logic;
+        clk_o_2      : out std_logic;
+        clk_o_3      : out std_logic;
+        reset        : in  std_logic                    := '0';
+        standby      : in  std_logic                    := '0';
+        phasesel     : in  std_logic_vector(1 downto 0) := (others => '0');
+        phasedir     : in  std_logic                    := '0';
+        phasestep    : in  std_logic                    := '0';
+        phaseloadreg : in  std_logic                    := '0';
+        locked       : out std_logic
+    );
+end entity ecp5pll;
+
+architecture RTL of ecp5pll is
+    constant PFD_MIN     : positive := 3125000 / G_FREQ_SCALE;
+    constant PFD_MAX     : positive := 400000000 / G_FREQ_SCALE;
+    constant VCO_MIN     : positive := 400000000 / G_FREQ_SCALE;
+    constant VCO_MAX     : positive := 800000000 / G_FREQ_SCALE;
+    constant VCO_OPTIMAL : positive := (VCO_MIN + VCO_MAX) / 2;
+
+    function get_error(out_freq, vco_freq : integer) return integer is
+    begin
+        if out_freq > 0 then
+            if vco_freq >= out_freq then
+                return abs (vco_freq / (vco_freq / out_freq) - out_freq);
+            else
+                return abs (vco_freq - out_freq);
+            end if;
+        end if;
+        return 0;
+    end function;
+
+    type T_DIVIDER is record
+        refclk, feedback, output : integer;
+    end record;
+
+    function GET_DIVS return T_DIVIDER is
+        variable input_div_min, input_div_max                     : integer;
+        variable output_div_min, output_div_max                   : integer;
+        variable feedback_div, feedback_div_min, feedback_div_max : integer;
+        variable fvco_tmp, fvco_tmp1, fout_tmp                    : integer;
+        variable error, error_prev                                : integer;
+        variable ret                                              : T_DIVIDER;
+    begin
+
+        fvco_tmp1     := 0;
+        error_prev    := 999999999;
+        input_div_min := maximum(1, G_IN_HZ / PFD_MAX);
+        input_div_max := minimum(128, G_IN_HZ / PFD_MIN);
+        for input_div in input_div_min to input_div_max loop
+            if (G_OUT_0_HZ / (1000000 / G_FREQ_SCALE) * input_div < 2000) then
+                feedback_div := G_OUT_0_HZ * input_div / G_IN_HZ;
+            else
+                feedback_div := G_OUT_0_HZ / G_IN_HZ * input_div;
+            end if;
+            feedback_div_min := maximum(1, feedback_div);
+            feedback_div_max := minimum(80, feedback_div + 1);
+            for feedback_div in feedback_div_min to feedback_div_max loop
+                output_div_min := maximum(1, (VCO_MIN / feedback_div) / (G_IN_HZ / input_div));
+                output_div_max := minimum(128, (VCO_MAX / feedback_div) / (G_IN_HZ / input_div));
+                fout_tmp       := G_IN_HZ * feedback_div / input_div;
+                for output_div in output_div_min to output_div_max loop
+                    fvco_tmp := fout_tmp * output_div;
+                    error    := abs (fout_tmp - G_OUT_0_HZ) + get_error(G_OUT_1_HZ, fvco_tmp) + get_error(G_OUT_2_HZ, fvco_tmp) + get_error(G_OUT_3_HZ, fvco_tmp);
+                    if (error < error_prev or (error = error_prev and abs (fvco_tmp - VCO_OPTIMAL) < abs (fvco_tmp1 - VCO_OPTIMAL))) then
+                        error_prev := error;
+                        ret        := (
+                            refclk   => input_div,
+                            feedback => feedback_div,
+                            output   => output_div
+                        );
+                        fvco_tmp1  := fvco_tmp;
+                    end if;
+                end loop;
+            end loop;
+        end loop;
+        return ret;
+    end function;
+
+    function F_primary_phase(output_div, deg : integer) return integer is
+        variable phase_compensation, phase_count_x8 : integer;
+    begin
+        phase_compensation := (output_div + 1) / 2 * 8 - 8 + output_div / 2 * 8; -- output_div/2*8 = 180 deg shift
+        phase_count_x8     := phase_compensation + 8 * output_div * deg / 360;
+        if (phase_count_x8 > 1023) then
+            phase_count_x8 := phase_count_x8 mod (output_div * 8); -- wraparound 360 deg
+        end if;
+        return phase_count_x8;
+    end function;
+
+    constant DIVS : T_DIVIDER := GET_DIVS;
+    constant FOUT : integer   := G_IN_HZ * DIVS.feedback / DIVS.refclk;
+    constant FVCO : integer   := FOUT * DIVS.output;
+
+    constant PRIMARY_CPHASE : integer := F_primary_phase(DIVS.output, G_OUT_0_DEG) / 8;
+    constant PRIMARY_FPHASE : integer := F_primary_phase(DIVS.output, G_OUT_0_DEG) mod 8;
+
+    function secondary_divisor(sfreq : integer) return integer is
+    begin
+        if (sfreq > 0) and (FVCO > sfreq) then
+            return FVCO / sfreq;
+        end if;
+        return 1;
+    end function;
+
+    function enabled_str(cond : boolean) return string is
+    begin
+        if cond then
+            return "ENABLED";
+        end if;
+        return "DISABLED";
+    end function;
+
+    function secondary_phase(sfreq, sphase : integer) return integer is
+        variable phase_compensation, phase_count_x8 : integer := 0;
+        variable div                                : integer := 1;
+    begin
+        if (sfreq > 0) then
+            if (FVCO >= sfreq) then
+                div := FVCO / sfreq;
+            end if;
+            -- freq               := params_fvco / div;
+            phase_compensation := div * 8 - 8;
+            phase_count_x8     := phase_compensation + 8 * div * sphase / 360;
+            if (phase_count_x8 > 1023) then
+                phase_count_x8 := phase_count_x8 mod (div * 8); -- wraparound 360 deg
+            end if;
+        end if;
+
+        return phase_count_x8;
+    end function;
+
+    constant SECONDARY_DIV_1    : integer := secondary_divisor(G_OUT_1_HZ);
+    constant SECONDARY_CPHASE_1 : integer := secondary_phase(G_OUT_1_HZ, G_OUT_1_DEG) / 8;
+    constant SECONDARY_FPHASE_1 : integer := secondary_phase(G_OUT_1_HZ, G_OUT_1_DEG) mod 8;
+    constant SECONDARY_DIV_2    : integer := secondary_divisor(G_OUT_2_HZ);
+    constant SECONDARY_CPHASE_2 : integer := secondary_phase(G_OUT_2_HZ, G_OUT_2_DEG) / 8;
+    constant SECONDARY_FPHASE_2 : integer := secondary_phase(G_OUT_2_HZ, G_OUT_2_DEG) mod 8;
+    constant SECONDARY_DIV_3    : integer := secondary_divisor(G_OUT_3_HZ);
+    constant SECONDARY_CPHASE_3 : integer := secondary_phase(G_OUT_3_HZ, G_OUT_3_DEG) / 8;
+    constant SECONDARY_FPHASE_3 : integer := secondary_phase(G_OUT_3_HZ, G_OUT_3_DEG) mod 8;
+
+    component                           -- attribute: "blackbox"
+        EHXPLLL is                      -- @suppress "Component declaration 'EHXPLLL' has none or multiple matching entity declarations"
+        generic(
+            CLKI_DIV         : integer := 1;
+            CLKFB_DIV        : integer := 1;
+            CLKOP_DIV        : integer := 8;
+            CLKOS_DIV        : integer := 8;
+            CLKOS2_DIV       : integer := 8;
+            CLKOS3_DIV       : integer := 8;
+            CLKOP_ENABLE     : string  := "ENABLED";
+            CLKOS_ENABLE     : string  := "DISABLED";
+            CLKOS2_ENABLE    : string  := "DISABLED";
+            CLKOS3_ENABLE    : string  := "DISABLED";
+            CLKOP_CPHASE     : integer := 0;
+            CLKOS_CPHASE     : integer := 0;
+            CLKOS2_CPHASE    : integer := 0;
+            CLKOS3_CPHASE    : integer := 0;
+            CLKOP_FPHASE     : integer := 0;
+            CLKOS_FPHASE     : integer := 0;
+            CLKOS2_FPHASE    : integer := 0;
+            CLKOS3_FPHASE    : integer := 0;
+            FEEDBK_PATH      : string  := "CLKOP";
+            CLKOP_TRIM_POL   : string  := "RISING";
+            CLKOP_TRIM_DELAY : integer := 0;
+            CLKOS_TRIM_POL   : string  := "RISING";
+            CLKOS_TRIM_DELAY : integer := 0;
+            OUTDIVIDER_MUXA  : string  := "DIVA";
+            OUTDIVIDER_MUXB  : string  := "DIVB";
+            OUTDIVIDER_MUXC  : string  := "DIVC";
+            OUTDIVIDER_MUXD  : string  := "DIVD";
+            PLL_LOCK_MODE    : integer := 0;
+            PLL_LOCK_DELAY   : integer := 200;
+            STDBY_ENABLE     : string  := "DISABLED";
+            REFIN_RESET      : string  := "DISABLED";
+            SYNC_ENABLE      : string  := "DISABLED";
+            INT_LOCK_STICKY  : string  := "ENABLED";
+            DPHASE_SOURCE    : string  := "DISABLED";
+            PLLRST_ENA       : string  := "DISABLED";
+            INTFB_WAKE       : string  := "DISABLED"
+        );
+        port(
+            CLKI         : in  std_logic;
+            CLKFB        : in  std_logic;
+            PHASESEL1    : in  std_logic;
+            PHASESEL0    : in  std_logic;
+            PHASEDIR     : in  std_logic;
+            PHASESTEP    : in  std_logic;
+            PHASELOADREG : in  std_logic;
+            STDBY        : in  std_logic;
+            PLLWAKESYNC  : in  std_logic;
+            RST          : in  std_logic;
+            ENCLKOP      : in  std_logic;
+            ENCLKOS      : in  std_logic;
+            ENCLKOS2     : in  std_logic;
+            ENCLKOS3     : in  std_logic;
+            CLKOP        : out std_logic;
+            CLKOS        : out std_logic;
+            CLKOS2       : out std_logic;
+            CLKOS3       : out std_logic;
+            LOCK         : out std_logic;
+            INTLOCK      : out std_logic;
+            REFCLK       : out std_logic;
+            CLKINTFB     : out std_logic
+        );
+
+    end component;
+
+    attribute ICP_CURRENT            : string;
+    attribute LPF_RESISTOR           : string;
+    attribute MFG_ENABLE_FILTEROPAMP : string;
+    attribute MFG_GMCREF_SEL         : string;
+    attribute ICP_CURRENT of PLL_INST : label is "12";
+    attribute LPF_RESISTOR of PLL_INST : label is "8";
+    attribute MFG_ENABLE_FILTEROPAMP of PLL_INST : label is "1";
+    attribute MFG_GMCREF_SEL of PLL_INST : label is "2";
+
+    signal PHASESEL_HW : unsigned(phasesel'range);
+begin
+
+    assert abs (G_OUT_0_HZ - FOUT) <= G_OUT_0_TOL_HZ --
+    report "cannot generate output frequency for clk_O_0.  G_OUT_0_HZ - params_fout: " & integer'image(G_OUT_0_HZ - FOUT) & " is higer than specified tolerance"
+    severity FAILURE;
+    assert G_OUT_1_HZ = 0 or abs (G_OUT_1_HZ - FVCO / SECONDARY_DIV_1) <= G_OUT_1_TOL_HZ --
+    report "cannot generate output frequency for clk_O_1.  diff: " & integer'image(abs (G_OUT_1_HZ - FVCO / SECONDARY_DIV_1)) & " is higer than specified tolerance"
+    severity FAILURE;
+    assert G_OUT_2_HZ = 0 or abs (G_OUT_2_HZ - FVCO / SECONDARY_DIV_2) <= G_OUT_2_TOL_HZ --
+    report "cannot generate output frequency for clk_O_2.  diff: " & integer'image(abs (G_OUT_2_HZ - FVCO / SECONDARY_DIV_2)) & " is higer than specified tolerance"
+    severity FAILURE;
+    assert G_OUT_3_HZ = 0 or abs (G_OUT_3_HZ - FVCO / SECONDARY_DIV_3) <= G_OUT_3_TOL_HZ --
+    report "cannot generate output frequency for clk_O_3.  diff: " & integer'image(abs (G_OUT_3_HZ - FVCO / SECONDARY_DIV_3)) & " is higer than specified tolerance"
+    severity FAILURE;
+
+    PHASESEL_HW <= unsigned(phasesel) - 1;
+
+    assert FALSE report "EHXPLLL parameters:" & --
+    LF & "    CLKI_DIV   : " & integer'image(DIVS.refclk) & --
+    LF & "    CLKFB_DIV  : " & integer'image(DIVS.feedback) & --
+    LF & "    CLKOP_DIV  : " & integer'image(DIVS.output) & --
+    LF & "    CLKOS_DIV  : " & integer'image(SECONDARY_DIV_1) & --
+    LF & "    CLKOS2_DIV : " & integer'image(SECONDARY_DIV_2) & --
+    LF & "    CLKOS3_DIV : " & integer'image(SECONDARY_DIV_3) & --
+    LF severity NOTE;
+
+    PLL_INST : EHXPLLL
+        generic map(
+            CLKI_DIV         => DIVS.refclk,
+            CLKFB_DIV        => DIVS.feedback,
+            CLKOP_DIV        => DIVS.output,
+            CLKOS_DIV        => SECONDARY_DIV_1,
+            CLKOS2_DIV       => SECONDARY_DIV_2,
+            CLKOS3_DIV       => SECONDARY_DIV_3,
+            CLKOP_ENABLE     => "ENABLED",
+            CLKOS_ENABLE     => enabled_str(G_OUT_1_HZ > 0),
+            CLKOS2_ENABLE    => enabled_str(G_OUT_2_HZ > 0),
+            CLKOS3_ENABLE    => enabled_str(G_OUT_3_HZ > 0),
+            CLKOP_CPHASE     => PRIMARY_CPHASE,
+            CLKOS_CPHASE     => SECONDARY_CPHASE_1,
+            CLKOS2_CPHASE    => SECONDARY_CPHASE_2,
+            CLKOS3_CPHASE    => SECONDARY_CPHASE_3,
+            CLKOP_FPHASE     => PRIMARY_FPHASE,
+            CLKOS_FPHASE     => SECONDARY_FPHASE_1,
+            CLKOS2_FPHASE    => SECONDARY_FPHASE_2,
+            CLKOS3_FPHASE    => SECONDARY_FPHASE_3,
+            FEEDBK_PATH      => "CLKOP",
+            CLKOP_TRIM_POL   => "RISING",
+            CLKOP_TRIM_DELAY => 0,
+            CLKOS_TRIM_POL   => "RISING",
+            CLKOS_TRIM_DELAY => 0,
+            OUTDIVIDER_MUXA  => "DIVA",
+            OUTDIVIDER_MUXB  => "DIVB",
+            OUTDIVIDER_MUXC  => "DIVC",
+            OUTDIVIDER_MUXD  => "DIVD",
+            PLL_LOCK_MODE    => 0,
+            PLL_LOCK_DELAY   => 200,
+            STDBY_ENABLE     => enabled_str(G_STANDBY_EN),
+            REFIN_RESET      => "DISABLED",
+            SYNC_ENABLE      => "DISABLED",
+            INT_LOCK_STICKY  => "ENABLED",
+            DPHASE_SOURCE    => enabled_str(G_DYNAMIC_EN),
+            PLLRST_ENA       => enabled_str(G_RESET_EN),
+            INTFB_WAKE       => "DISABLED"
+        )
+        port map(
+            CLKI         => clk_i,
+            CLKFB        => clk_o_0,
+            PHASESEL1    => PHASESEL_HW(1),
+            PHASESEL0    => PHASESEL_HW(0),
+            PHASEDIR     => phasedir,
+            PHASESTEP    => phasestep,
+            PHASELOADREG => phaseloadreg,
+            STDBY        => '0',
+            PLLWAKESYNC  => '0',
+            RST          => '0',
+            ENCLKOP      => '0',
+            ENCLKOS      => '0',
+            ENCLKOS2     => '0',
+            ENCLKOS3     => '0',
+            CLKOP        => clk_o_0,
+            CLKOS        => clk_o_1,
+            CLKOS2       => clk_o_2,
+            CLKOS3       => clk_o_3,
+            LOCK         => locked,
+            INTLOCK      => open,
+            REFCLK       => open,
+            CLKINTFB     => open
+        );
+
+end architecture;
diff --git a/examples/boards/ulx3s/blinky/xedaproject.toml b/examples/boards/ulx3s/blinky/xedaproject.toml
new file mode 100644
index 00000000..3ee42325
--- /dev/null
+++ b/examples/boards/ulx3s/blinky/xedaproject.toml
@@ -0,0 +1,47 @@
+[[design]]
+name = 'blinky'
+description = 'Running LEDs on the ULX3S board'
+
+[design.rtl]
+sources = ['blinky.v']
+top = 'blinky'
+clock_port = 'clk_25mhz'
+
+[[design]]
+name = 'blinky-vhdl'
+description = 'LED chaser on ULX3S'
+language.vhdl.standard = 2008
+
+[design.rtl]
+sources = [ #
+    "ecp5pll.vhdl",
+    'blinky.vhdl',
+]
+top = 'blinky'
+clock_port = 'clk_25mhz'
+
+
+[flows.openfpgaloader]
+board = 'ULX3S_85F'
+clock_period = 40   # 25 MHz
+nextpnr.detailed_timing_report = false
+nextpnr.timing_allow_fail = true
+
+[flows.nextpnr]
+board = 'ULX3S_85F'
+clock_period = 40   # 25 MHz
+
+[flows.yosys]
+fpga.family = 'ecp5'
+clock_period = 40    #
+
+[flows.vivado_synth]
+fpga.part = 'xc7a12tcsg325-1'
+clock_period = 4.0
+synth.strategy = 'Flow_PerfOptimized_high'
+impl.strategy = 'Performance_Explore'
+blacklisted_resources = ['bram', 'dsp']
+
+[flows.vivado_custom_synth]
+fpga.part = 'xc7a12tcsg325-1'
+clock_period = 4.0
diff --git a/examples/vhdl/full_adder.vhd b/examples/vhdl/full_adder.vhd
new file mode 100644
index 00000000..5990bab1
--- /dev/null
+++ b/examples/vhdl/full_adder.vhd
@@ -0,0 +1,17 @@
+library ieee;
+use ieee.std_logic_1164.all;
+
+entity full_adder is
+    port (
+        a  : in  std_logic;
+        b  : in  std_logic;
+        ci : in  std_logic;
+        s  : out std_logic;
+        co : out std_logic);
+end;
+
+architecture behavioral of full_adder is
+begin
+    s <= a xor b xor ci;
+    co <= (a and b) or (b and ci) or (ci and a);
+end;
\ No newline at end of file
diff --git a/examples/vhdl/full_adder_piped.vhd b/examples/vhdl/full_adder_piped.vhd
new file mode 100644
index 00000000..c9ae88cb
--- /dev/null
+++ b/examples/vhdl/full_adder_piped.vhd
@@ -0,0 +1,34 @@
+library ieee;
+use ieee.std_logic_1164.all;
+
+entity full_adder_piped is
+   port(
+      clk   : in  std_logic;
+      reset : in  std_logic;
+      in_a  : in  std_logic;
+      in_b  : in  std_logic;
+      in_ci : in  std_logic;
+      s     : out std_logic;
+      co    : out std_logic
+   );
+end;
+
+architecture behavioral of full_adder_piped is
+   signal a, b, ci : std_logic;
+begin
+   process(clk)
+   begin
+      if rising_edge(clk) then
+         if reset = '1' then
+            s  <= '0';                  -- not really needed
+            co <= '0';                  -- not really needed
+         else
+            a  <= in_a;
+            b  <= in_b;
+            ci <= in_ci;
+            s  <= a xor b xor ci;
+            co <= (a and b) or (b and ci) or (ci and a);
+         end if;                        -- reset
+      end if;                           -- clk rising_edge
+   end process;
+end;
diff --git a/examples/vhdl/full_adder_tb.vhd b/examples/vhdl/full_adder_tb.vhd
new file mode 100644
index 00000000..b48c133c
--- /dev/null
+++ b/examples/vhdl/full_adder_tb.vhd
@@ -0,0 +1,62 @@
+-------------------------------------------------------------------------------
+-- Testbench for full_adder
+-- Adopted from https://vhdlguide.readthedocs.io/en/latest/vhdl/testbench.html
+-------------------------------------------------------------------------------
+
+library ieee;
+use ieee.std_logic_1164.all;
+
+entity full_adder_tb is
+end full_adder_tb;
+
+architecture behavior of full_adder_tb is
+  component full_adder is
+    port (
+      a  : in std_logic;
+      b  : in std_logic;
+      ci : in std_logic;
+      s  : out std_logic;
+      co : out std_logic);
+  end component;
+  signal a, b, ci, s, co : std_logic;
+
+  type test_vector is record
+    a, b, ci : std_logic;
+    s, co    : std_logic;
+  end record;
+
+  type test_vector_array is array (natural range <>) of test_vector;
+  constant test_vectors : test_vector_array := (
+    -- a, b, ci, s , co
+    ('0', '0', '0', '0', '0'),
+    ('0', '0', '1', '1', '0'),
+    ('0', '1', '0', '1', '0'),
+    ('1', '0', '0', '1', '0'),
+    ('1', '1', '0', '0', '1'),
+    ('0', '1', '1', '0', '1'),
+    ('1', '0', '1', '0', '1'),
+    ('1', '1', '1', '1', '1')
+  );
+begin
+  uut : full_adder port map(
+    a  => a,
+    b  => b,
+    ci => ci,
+    s  => s,
+    co => co
+  );
+  stim_proc : process
+    constant delay : time := 10 ns;
+  begin
+    for i in test_vectors'range loop
+      a  <= test_vectors(i).a;
+      b  <= test_vectors(i).b;
+      ci <= test_vectors(i).ci;
+      wait for delay;
+      assert (s = test_vectors(i).s) and (co = test_vectors(i).co)
+      report "test_vector " & integer'image(i) & " failed" severity failure;
+    end loop;
+    report "full_adder_tb finished!";
+    wait;
+  end process;
+end;
\ No newline at end of file
diff --git a/examples/vhdl/pipeline/pipelined_adder.toml b/examples/vhdl/pipeline/pipelined_adder.toml
new file mode 100644
index 00000000..7f0fa5e1
--- /dev/null
+++ b/examples/vhdl/pipeline/pipelined_adder.toml
@@ -0,0 +1,13 @@
+name = 'pipelined_adder'
+description = 'Example VHDL design'
+language.vhdl.standard = 2008
+
+[rtl]
+sources = ['pipelined_adder.vhdl']
+top = 'pipelined_adder'
+clock_port = 'clock'
+
+[tb]
+sources = ['pipelined_adder_tb.vhdl']
+top = 'pipelined_adder_tb'
+uut = 'uut'
diff --git a/examples/vhdl/pipeline/pipelined_adder.vhdl b/examples/vhdl/pipeline/pipelined_adder.vhdl
new file mode 100644
index 00000000..2b1cce67
--- /dev/null
+++ b/examples/vhdl/pipeline/pipelined_adder.vhdl
@@ -0,0 +1,78 @@
+library ieee;
+use ieee.std_logic_1164.all;
+use ieee.numeric_std.all;
+
+entity pipelined_adder is
+   generic(
+      W : positive := 32
+   );
+   port(
+      clock     : in  std_logic;
+      reset     : in  std_logic;
+      in_a      : in  std_logic_vector(W - 1 downto 0);
+      in_b      : in  std_logic_vector(W - 1 downto 0);
+      in_valid  : in  std_logic;
+      in_ready  : out std_logic;
+      out_sum   : out std_logic_vector(W downto 0);
+      out_valid : out std_logic;
+      out_ready : in  std_logic
+   );
+end;
+
+architecture behavioral of pipelined_adder is
+   constant DEPTH : positive := 2;
+
+   -- registers
+   signal l0_a, l0_b : std_logic_vector(W - 1 downto 0);
+   signal l1_s       : std_logic_vector(W downto 0);
+   signal valid_pipe : std_logic_vector(0 to DEPTH - 1);
+
+   -- wires
+   signal valids  : std_logic_vector(0 to DEPTH);
+   signal readies : std_logic_vector(0 to DEPTH);
+begin
+
+   process(all)
+   begin
+      readies(DEPTH) <= out_ready;
+      in_ready       <= readies(0);
+      valids(0)      <= in_valid;
+      out_valid      <= valids(DEPTH);
+
+      for i in 0 to DEPTH - 1 loop
+         readies(i)    <= not valid_pipe(i) or readies(i + 1);
+         valids(i + 1) <= valid_pipe(i);
+      end loop;
+
+      out_sum <= l1_s;
+   end process;
+
+   process(clock)
+   begin
+      if rising_edge(clock) then
+         if reset = '1' then
+            valid_pipe <= (others => '0');
+         else
+            for i in 0 to DEPTH - 1 loop
+               if readies(i) then
+                  valid_pipe(i) <= valids(i);
+               end if;
+            end loop;
+         end if;
+      end if;
+   end process;
+
+   process(clock)
+   begin
+      if rising_edge(clock) then
+         -- Update logic for each stage: 
+         if readies(0) then
+            l0_a <= in_a;
+            l0_b <= in_b;
+         end if;
+         if readies(1) then
+            l1_s <= std_logic_vector(unsigned('0' & l0_a) + unsigned(l0_b));
+         end if;
+      end if;
+   end process;
+end;
diff --git a/examples/vhdl/pipeline/pipelined_adder_tb.vhdl b/examples/vhdl/pipeline/pipelined_adder_tb.vhdl
new file mode 100644
index 00000000..8a480067
--- /dev/null
+++ b/examples/vhdl/pipeline/pipelined_adder_tb.vhdl
@@ -0,0 +1,135 @@
+library ieee;
+use ieee.std_logic_1164.all;
+use ieee.numeric_std.all;
+
+entity pipelined_adder_tb is
+  generic(
+    G_CLOCK_PERIOD_PS   : positive := 15_000;
+    G_WAIT_PRE_RESET_NS : positive := 100
+  );
+end pipelined_adder_tb;
+
+architecture TB of pipelined_adder_tb is
+
+  constant W : positive := 32;
+  component pipelined_adder is
+    -- post-synthesis simulation wont work as netlist has no generics
+    -- generic(
+    --   W : positive := 32
+    -- );
+    port(
+      clock     : in  std_logic;
+      reset     : in  std_logic;
+      in_a      : in  std_logic_vector(W - 1 downto 0);
+      in_b      : in  std_logic_vector(W - 1 downto 0);
+      in_valid  : in  std_logic;
+      in_ready  : out std_logic;
+      out_sum   : out std_logic_vector(W downto 0);
+      out_valid : out std_logic;
+      out_ready : in  std_logic
+    );
+  end component;
+  constant clock_period : time := G_CLOCK_PERIOD_PS * ps;
+
+  signal in_ready, out_valid               : std_logic;
+  signal clock, reset, in_valid, out_ready : std_logic := '0';
+  signal stop_clock, reset_done            : boolean   := FALSE;
+  signal a, b                              : std_logic_vector(W - 1 downto 0);
+  signal s                                 : std_logic_vector(W downto 0);
+
+  type test_vector is record
+    a, b : std_logic_vector(W - 1 downto 0);
+    s    : std_logic_vector(W downto 0);
+  end record;
+
+  type test_vector_array is array (natural range <>) of test_vector;
+  constant test_vectors : test_vector_array := (
+    -- a, b, s
+    (32X"111", 32X"022", 33X"133"),
+    (32X"244", 32X"122", 33X"366"),
+    (32X"311", 32X"0ab", 33X"3bc"),
+    (32X"222", 32X"555", 33X"777")
+  );
+begin
+  uut : pipelined_adder
+      -- generic map(
+      --   W => W
+      -- )
+    port map(
+      clock     => clock,
+      reset     => reset,
+      in_a      => a,
+      in_b      => b,
+      in_valid  => in_valid,
+      in_ready  => in_ready,
+      out_sum   => s,
+      out_valid => out_valid,
+      out_ready => out_ready
+    );
+  -- generate clock
+  CLOCK_PROCESS : process
+  begin
+    clock <= '0';
+    if not stop_clock then
+      clock <= not clock;
+      wait for clock_period / 2;
+    else
+      wait;
+    end if;
+  end process;
+
+  RESET_PROCESS : process
+  begin
+    reset      <= '0';
+    wait for G_WAIT_PRE_RESET_NS * ns;
+    wait until falling_edge(clock);
+    reset      <= '1';
+    wait for clock_period;
+    reset      <= not reset;
+    wait until rising_edge(clock);
+    report "RESET DONE" severity note;
+    reset_done <= True;
+    wait;
+  end process;
+
+  STIMULUS_PROCESS : process
+  begin
+    in_valid <= '0';
+    wait until reset_done;
+    for i in test_vectors'range loop
+      a        <= test_vectors(i).a;
+      b        <= test_vectors(i).b;
+      in_valid <= '1';
+      loop
+        wait until rising_edge(clock);
+        if in_ready then
+          exit;
+        end if;
+      end loop;
+      in_valid <= '0';
+    end loop;
+    report "End of Inputs";
+    wait;
+  end process;
+
+  MONITOR_PROCESS : process
+  begin
+    out_ready  <= '0';
+    wait until reset_done;
+    for i in test_vectors'range loop
+      out_ready <= '1';
+      loop
+        wait until rising_edge(clock);
+        if out_valid then
+          exit;
+        end if;
+      end loop;
+      report "test_vector " & integer'image(i) & " a=" & to_hstring(test_vectors(i).a) & " b=" & to_hstring(test_vectors(i).b) & " received: " & to_hstring(s) severity NOTE;
+      assert s = test_vectors(i).s report "test_vector " & integer'image(i) & " failed. Output: " & to_hstring(s) & " Expected:" & to_hstring(test_vectors(i).s) severity failure;
+      out_ready <= '0';
+    end loop;
+    report "full_adder_tb finished!";
+    stop_clock <= TRUE;
+    wait;
+  end process;
+end;
diff --git a/examples/vhdl/pipeline/synthesize.py b/examples/vhdl/pipeline/synthesize.py
new file mode 100644
index 00000000..fb2ef591
--- /dev/null
+++ b/examples/vhdl/pipeline/synthesize.py
@@ -0,0 +1,21 @@
+from subprocess import check_call
+
+
+def quartus_synth():
+    check_call(
+        [
+            "xeda",
+            "run",
+            "quartus",
+            "--design",
+            "pipelined_adder.toml",
+            "--settings",
+            "dockerized=true",
+            "fpga.part=10CL016YU256C6G",
+            "clock_period=15",
+        ]
+    )
+
+
+if __name__ == "__main__":
+    quartus_synth()
diff --git a/examples/vhdl/sqrt/requirements.txt b/examples/vhdl/sqrt/requirements.txt
new file mode 100644
index 00000000..d06645d5
--- /dev/null
+++ b/examples/vhdl/sqrt/requirements.txt
@@ -0,0 +1,1 @@
+cocolight>=0.0.2
\ No newline at end of file
diff --git a/examples/vhdl/sqrt/sqrt.toml b/examples/vhdl/sqrt/sqrt.toml
new file mode 100644
index 00000000..716f92cc
--- /dev/null
+++ b/examples/vhdl/sqrt/sqrt.toml
@@ -0,0 +1,15 @@
+name = "sqrt"
+description = "Integer Square Root"
+language.vhdl.standard = "2008"
+
+[rtl]
+sources = ["sqrt.vhdl"]
+top = "sqrt"
+clock_port = "clk"
+parameters = { G_IN_WIDTH = 32 }
+# parameters = { G_IN_WIDTH = 32, G_ITERATIVE = true, G_STR= "abcd", G_BITVECTOR="7'b0101001" }
+
+[tb]
+sources = ["tb_sqrt.py"]
+cocotb = true
+# top = "tb_sqrt"  # FIXME
diff --git a/examples/vhdl/sqrt/sqrt.vhdl b/examples/vhdl/sqrt/sqrt.vhdl
new file mode 100644
index 00000000..3aeaa685
--- /dev/null
+++ b/examples/vhdl/sqrt/sqrt.vhdl
@@ -0,0 +1,124 @@
+--===============================================================================================--
+--! @brief         Compute square-root of an integer
+--! @author        Kamyar Mohajerani (kamyar@ieee.org)
+--! @copyright     Copyright (c) 2022
+--! @license       Solderpad Hardware License v2.1 (SHL-2.1)
+--! @brief         Compute square-root of an integer
+--! @vhdl          VHDL 2008
+--!
+--! @description   
+--!                Based on the SystemVerilog implementation by Will Green 
+--!                  at https://github.com/projf/projf-explore/blob/master/lib/maths/sqrt_int.sv
+--!
+--! @parameters    G_IN_WIDTH: width of input
+--!
+--===============================================================================================--
+
+library ieee;
+use ieee.std_logic_1164.all;
+use ieee.numeric_std.all;
+
+entity sqrt is
+    generic(
+        G_IN_WIDTH : positive := 8           --@ Input width
+    );
+
+    port(
+        clk            : in  std_logic;
+        rst            : in  std_logic;
+        radicand       : in  std_logic_vector(G_IN_WIDTH - 1 downto 0);
+        radicand_valid : in  std_logic;
+        radicand_ready : out std_logic;
+        root           : out std_logic_vector((G_IN_WIDTH + 1) / 2 - 1 downto 0);
+        root_remainder : out std_logic_vector((G_IN_WIDTH + 1) / 2 downto 0);
+        root_valid     : out std_logic;
+        root_ready     : in  std_logic
+    );
+end entity sqrt;
+
+architecture RTL of sqrt is
+
+    --======================================== Constants ========================================--
+    constant ITER : natural := (G_IN_WIDTH + 1) / 2;
+    constant W    : natural := ITER * 2;
+    --========================================== Types ==========================================--
+    type T_STATE is (S_IDLE, S_BUSY, S_DONE);
+    --======================================== Functions ========================================--
+    function clog2(n : positive) return natural is
+        variable r    : natural  := 0;
+        variable pow2 : positive := 1;
+    begin
+        while n > pow2 loop
+            pow2 := pow2 * 2;
+            r    := r + 1;
+        end loop;
+        return r;
+    end function;
+
+    --======================================== Registers ========================================--
+    signal state        : T_STATE;
+    signal counter      : unsigned(clog2(ITER) - 1 downto 0);
+    signal acc          : unsigned(W + 1 downto 0);
+    signal q            : unsigned(W - 1 downto 0);
+    signal x            : unsigned(W - 3 downto 0);
+    --========================================== Wires ==========================================--
+    signal test_res     : unsigned(W + 1 downto 0);
+    signal test_res_msb : std_logic;
+begin
+    test_res       <= acc - (q & "01");
+    test_res_msb   <= test_res(test_res'length - 1);
+    root           <= std_logic_vector(resize(q, root'length));
+    root_remainder <= std_logic_vector(acc(root_remainder'length + 1 downto 2));
+
+    process(all)
+    begin
+        radicand_ready <= '0';
+        root_valid     <= '0';
+        case state is
+            when S_IDLE =>
+                radicand_ready <= '1';
+            when S_BUSY =>
+            when S_DONE =>
+                root_valid <= '1';
+        end case;
+    end process;
+
+    process(clk)
+    begin
+        if rising_edge(clk) then
+            if rst then
+                state <= S_IDLE;
+            else
+                case state is
+                    when S_IDLE =>
+                        if radicand_valid = '1' then
+                            counter <= (others => '0');
+                            q       <= (others => '0');
+                            -- (acc, x) <= resize(unsigned(radicand), 2 * W);
+                            x       <= unsigned(radicand(W - 3 downto 0));
+                            acc     <= resize(unsigned(radicand(radicand'length - 1 downto W - 2)), W + 2);
+                            state   <= S_BUSY;
+                        end if;
+                    when S_BUSY =>
+                        counter <= counter + 1;
+                        if test_res_msb = '1' then -- test_res < 0
+                            -- (acc, x) <= acc(acc'length - 3 downto 0) & x & "00";
+                            acc <= acc(acc'length - 3 downto 0) & x(W - 3 downto W - 4);
+                        else
+                            -- (acc, x) <= test_res(acc'length - 3 downto 0) & x & "00";
+                            acc <= test_res(acc'length - 3 downto 0) & x(W - 3 downto W - 4);
+                        end if;
+                        q <= q(q'length - 2 downto 0) & not test_res_msb;
+                        x <= unsigned(x(W - 5 downto 0)) & "00";
+                        if counter = ITER - 1 then
+                            state <= S_DONE;
+                        end if;
+                    when S_DONE =>
+                        if root_ready then
+                            state <= S_IDLE;
+                        end if;
+                end case;
+            end if;
+        end if;
+    end process;
+end architecture;
diff --git a/examples/vhdl/sqrt/tb_sqrt.py b/examples/vhdl/sqrt/tb_sqrt.py
new file mode 100644
index 00000000..9689b77a
--- /dev/null
+++ b/examples/vhdl/sqrt/tb_sqrt.py
@@ -0,0 +1,61 @@
+import os
+import random
+
+import cocotb
+from box import Box
+from cocolight import DUT, DutClock, DutReset, ValidReadyTb, cocotest
+
+NUM_TV = int(os.environ.get("NUM_TV", 2000))
+DEBUG = bool(os.environ.get("DEBUG", False))
+
+
+class SqrtTb(ValidReadyTb):
+    def __init__(self, dut: DUT, debug: bool = DEBUG):
+        super().__init__(dut, DutClock("clk"), DutReset("rst"), debug)
+        self.in_bus = self.driver("radicand", data_suffix=None)
+        self.out_bus = self.monitor("root", data_suffix=[None, "remainder"])
+
+    async def verify(self, rad: int):
+        stimulus = cocotb.start_soon(self.in_bus.enqueue(rad))
+        out = await self.out_bus.dequeue()
+        await stimulus
+
+        assert isinstance(out, Box)
+        root = int(out[None])
+        remainder = int(out.remainder)
+
+        assert rad == root**2 + remainder, f"{rad} !=  {root} ** 2 + {remainder}"
+        assert rad < (root + 1) ** 2, "returned root was smaller than expected"
+
+        self.log.debug("radicand=%d got root=%d remainder=%d", rad, root, remainder)
+
+
+@cocotest()
+async def test_sqrt_corners(dut: DUT, debug=False):
+    tb = SqrtTb(dut, debug=debug)
+    await tb.reset()
+    tb.log.info("DUT: %s", str(dut))
+    # get bound parameters/generics from the simulator
+    G_IN_WIDTH = tb.get_int_value("G_IN_WIDTH")
+    tb.log.info(f"G_IN_WIDTH:{G_IN_WIDTH}")
+    # await tb.clock_edge
+    # TODO make sure ranges are within G_IN_WIDTH bits
+    assert G_IN_WIDTH and G_IN_WIDTH >= 6
+    testcases = list(range(30)) + list(range(2**G_IN_WIDTH - 70, 2**G_IN_WIDTH))
+    testcases += list(range(2**G_IN_WIDTH // 2 - 30, 2**G_IN_WIDTH // 2 + 30))
+    for rad in testcases:
+        await tb.verify(rad)
+
+
+@cocotest
+async def test_sqrt(dut: DUT, num_tests: int = NUM_TV, debug=DEBUG):
+    tb = SqrtTb(dut, debug=debug)
+    await tb.reset()
+    # get bound parameters/generics from the simulator
+    G_IN_WIDTH = tb.get_value("G_IN_WIDTH", int)
+    tb.log.info("G_IN_WIDTH:%d num_tests:%d", G_IN_WIDTH, num_tests)
+    # await tb.clock_edge
+    testcases = [random.getrandbits(G_IN_WIDTH) for _ in range(num_tests)]
+    testcases = list(set(testcases))
+    for rad in testcases:
+        await tb.verify(rad)
diff --git a/examples/vhdl/xedaproject.toml b/examples/vhdl/xedaproject.toml
new file mode 100644
index 00000000..eb8af12f
--- /dev/null
+++ b/examples/vhdl/xedaproject.toml
@@ -0,0 +1,30 @@
+[[design]]
+name = 'full-adder'
+description = 'Example VHDL design'
+[design.rtl]
+sources = ['full_adder.vhd']
+top = 'full_adder'
+clock_port = ''
+[design.tb]
+sources = ['full_adder_tb.vhd']
+top = 'full_adder_tb'
+
+
+[flows.ghdl_sim]
+warn_error = true
+
+[flows.vivado_prj_synth]
+fpga.part = 'xc7a12tcsg325-1'
+clock_period = 5.0
+
+[flows.vivado_synth]
+fpga.part = 'xc7a12tcsg325-1'
+clock_period = 5.0
+
+[flows.ise_synth]
+fpga.family = "virtex5"
+fpga.device = "XC5VLX50"
+fpga.speed = -1
+fpga.package = "FFG676C"
+clock_period = 5.0
+
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..01379308
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,165 @@
+[project]
+name = "xeda"
+description = "Cross EDA Abstraction and Automation"
+readme = "README.md"
+requires-python = ">=3.8"
+dynamic = ["version", "entry-points"]
+license = { file = "LICENSE.txt" }
+keywords = [
+  "EDA",
+  "Electronic",
+  "Design",
+  "Automation",
+  "Tool",
+  "Synthesis",
+  "Simulation",
+  "Simulator",
+  "Verification",
+  "Hardware",
+  "Verilog",
+  "VHDL",
+  "FPGA",
+  "ASIC",
+]
+authors = [ #
+  { name = "Kamyar Mohajerani" },
+  { email = "kammoh@gmail.com" },
+]
+classifiers = [
+  "Programming Language :: Python",
+  "Development Status :: 4 - Beta",
+  "License :: OSI Approved :: Apache Software License",
+  "Intended Audience :: Science/Research",
+  "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
+  "Topic :: Utilities",
+]
+dependencies = [
+  # core
+  "jinja2 >= 3",
+  "pydantic >= 1.8.2",                       # dataclass
+  "tomli >= 1.1.0; python_version < '3.11'",
+  "python-box[] ~= 6.0",
+  # experimental core
+  "attrs ~= 21.4",         # dataclass alternative
+  "pyyaml >= 5.3",         # alternative design/project format (TODO, currently CLI only)
+  "typeguard >= 2.13",
+  "varname >= 0.8",
+  "overrides >= 0.1",
+  "pathvalidate >= 2.5.0",
+  # console/CLI
+  "click ~= 8.1",
+  "click_help_colors >= 0.9.1",
+  "coloredlogs >= 15.0.1",
+  "rich >= 11.2.0, <13",
+  # experimental CLI
+  "simple_term_menu>=1.4",
+  # misc, experimental
+  "numpy >= 1.19.5",
+  "pebble >= 4.6.3",
+  "psutil >= 5.9.0, <6.0",
+  "junitparser >= 2.4.2, <3",
+  "pyvcd >= 0.3",
+]
+
+[project.optional-dependencies]
+test = ["pytest > 7", "pytest-cov"]
+
+[project.urls]
+homepage = "https://github.com/XedaHQ/xeda"
+repository = "https://github.com/XedaHQ/xeda.git"
+changelog = "https://github.com/XedaHQ/xeda/blob/dev/CHANGELOG.md"
+# documentation = "..."
+
+# [project.entry-points.console-scripts]
+# xeda = "xeda:cli.cli"
+
+[build-system]
+requires = ["setuptools >= 61", "wheel", "setuptools_scm[toml] >= 6.2"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools_scm]
+
+[tool.pylint]
+
+[tool.pylint.master]
+ignore-paths = ["src/xeda/plugins/lwc/.*"]
+
+[tool.pylint.messages_control]
+max-line-length = 120
+disable = [
+  "line-too-long",
+  "invalid-name",
+  "too-few-public-methods",
+  "unspecified-encoding",
+  "missing-function-docstring",
+  "too-many-instance-attributes",
+  "too-many-statements",
+  "too-many-branches",
+  "too-many-locals",
+  "too-many-arguments",
+  "too-many-nested-blocks",
+  # to be removed:
+  "missing-module-docstring",
+  "unused-argument",
+  "fixme",
+  "f-string-without-interpolation",
+  "wrong-import-order",
+  "missing-class-docstring",
+  "no-self-argument",               # from pydantic validators (classmethod)
+  "no-self-use",                    # from pydantic validators (classmethod)
+]
+[tool.pylint.similarities]
+min-similarity-lines = 10
+
+# Ignore comments when computing similarities.
+ignore-comments = "yes"
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings = "yes"
+
+# Ignore imports when computing similarities.
+ignore-imports = "no"
+
+[tool.pylint.design]
+max-returns = 8
+
+[tool.bandit]
+skips = [
+  "B701",
+  "B101:assert_used",
+  "B404:blacklist",   # subprocess module
+  "B314",
+]
+# exclude = []
+
+[tool.flakeheaven.plugins]
+# include everything in pyflakes except F401
+# pyflakes = ["+*", "-F401"]
+pyflakes = ["+*"]
+pycodestyle = [
+  "+*",
+  "-E501", # line too long
+  "-E203",
+  "-E231", # whitespace before/after ':'
+  "-C812", # missing trailing comma
+  "-W503", # line break before binary operator 
+]
+# enable only codes from S100 to S199
+# flake8-bandit = ["-*", "+S1??"]
+flake8-bandit = [
+  "+*",
+  "-S603",
+  "-S404", # subprocess
+  "-S405",
+  "-S314", # xml.etree.ElementTree.parse
+  "-S101", # asserts
+  "-S311", # random crypto
+  "-S701", # jinja2 autoescape
+  "-S310", # urllib open URL
+]
+# enable everything that starts from `flake8-`
+"flake8-*" = ["+*"]
+# explicitly disable plugin
+flake8-docstrings = ["-*"]
+flake8-quotes = ["-*"]
+flake8-commas = ["-*"]
diff --git a/pytype.cfg b/pytype.cfg
new file mode 100644
index 00000000..dcc46878
--- /dev/null
+++ b/pytype.cfg
@@ -0,0 +1,76 @@
+# NOTE: All relative paths are relative to the location of this file.
+
+[pytype]
+
+# Space-separated list of files or directories to exclude.
+exclude =
+    **/*_test.py
+    **/test_*.py
+
+# Space-separated list of files or directories to process.
+inputs = xeda
+
+# Keep going past errors to analyze as many files as possible.
+keep_going = False
+
+# Run N jobs in parallel. When 'auto' is used, this will be equivalent to the
+# number of CPUs on the host system.
+jobs = auto
+
+# All pytype output goes here.
+output = .pytype
+
+# Paths to source code directories, separated by ':'.
+pythonpath =
+    src
+
+# Python version (major.minor) of the target code.
+python_version = 3.9
+
+# Use the enum overlay for more precise enum checking. This flag is temporary
+# and will be removed once this behavior is enabled by default.
+use_enum_overlay = Use the enum overlay for more precise enum checking.
+
+# Build dict literals from dict(k=v, ...) calls. This flag is temporary and will
+# be removed once this behavior is enabled by default.
+build_dict_literals_from_kwargs = Build dict literals from dict(k=v, ...) calls.
+
+# Enable stricter namedtuple checks, such as unpacking and 'typing.Tuple'
+# compatibility. This flag is temporary and will be removed once this behavior
+# is enabled by default.
+strict_namedtuple_checks = Enable stricter namedtuple checks, such as unpacking and 'typing.Tuple' compatibility.
+
+# Enable exhaustive checking of function parameter types. This flag is temporary
+# and will be removed once this behavior is enabled by default.
+strict_parameter_checks = Enable exhaustive checking of function parameter types.
+
+# Enable support for TypedDicts. This flag is temporary and will be removed once
+# this behavior is enabled by default.
+enable_typed_dicts = Enable support for TypedDicts.
+
+# Trace class creation for better type analysis of class bodies. This flag is
+# temporary and will be removed once this behavior is enabled by default.
+trace_all_classes = Trace class creation for better type analysis of class bodies.
+
+# Emit errors for comparisons between incompatible primitive types. This flag is
+# temporary and will be removed once this behavior is enabled by default.
+strict_primitive_comparisons = Emit errors for comparisons between incompatible primitive types.
+
+# Solve unknown types to label with structural types. This flag is temporary and
+# will be removed once this behavior is enabled by default.
+protocols = Solve unknown types to label with structural types.
+
+# Only load submodules that are explicitly imported. This flag is temporary and
+# will be removed once this behavior is enabled by default.
+strict_import = Only load submodules that are explicitly imported.
+
+# Infer precise return types even for invalid function calls. This flag is
+# temporary and will be removed once this behavior is enabled by default.
+precise_return = Infer precise return types even for invalid function calls.
+
+# Comma or space separated list of error names to ignore.
+disable =
+    pyi-error
+
+# Don't report errors.
+report_errors = True
diff --git a/setup.cfg b/setup.cfg
index 224a7795..e9557cc9 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,2 +1,81 @@
 [metadata]
-description-file = README.md
\ No newline at end of file
+name = xeda
+
+[options]
+include_package_data = True
+package_dir = =src
+packages = find:
+
+[options.extras_require]
+test = pytest
+docs = 
+    sphinx
+
+[options.entry_points]
+console_scripts =
+    xeda = xeda.cli:cli
+
+[options.package_data]
+* = *.xdc, *.sdf, *.sdc, *.dse, *.fdc, *.ldc, *.tcl, *.json, *.ys
+
+[options.packages.find]
+where = src
+
+[tool:pytest]
+minversion = 7.1
+addopts =
+    --verbose
+    --doctest-modules
+testpaths =
+    examples/
+    tests/
+python_files =
+    test_*.py
+    test.py
+
+[pycodestyle]
+max-line-length = 120
+# extend-ignore = D,E203 # D: Docstring errors, E203: see https://github.com/PyCQA/pycodestyle/issues/373 
+ignore = E501
+indent-size = 4
+exclude =
+    .github
+    .eggs
+
+[pydocstyle]
+ignore = D100,D102,D103,D105,D202,D203,D213,D400,D405,D406,D407,D413,D415
+
+[pylint]
+disable =
+    C0103,C0301,C0103,C0116,R0201,R0801,R0903,R0912,R0913,R0914,R0915,W0702
+ignore-docstrings = yes
+output-format = colorized
+
+[mypy]
+ignore_missing_imports = True
+# namespace_packages = True
+# explicit-package-bases = True
+exclude =
+    (?x)(
+        ^src/xeda/plugins/.*
+        | ^build/.*
+        | ^.tox/.*
+        | ^examples/.*
+    )
+warn_unused_configs = True
+check_untyped_defs = True
+color_output = False
+pretty = False
+follow_imports = skip
+## WIP:
+# strict = True
+## for future:
+# disallow_untyped_calls = True
+# disallow_untyped_defs = True
+# disallow_incomplete_defs = True
+# disallow_untyped_decorators = True
+
+[flake8]
+ignore = D203,E501,F541,W503
+max-complexity = 22
+max-line-length = 120
\ No newline at end of file
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 0093001a..00000000
--- a/setup.py
+++ /dev/null
@@ -1,108 +0,0 @@
-from setuptools import setup, find_packages
-from setuptools.command.install import install
-from setuptools.command.develop import develop
-
-
-def _post_install(dir):
-    from xeda.xeda_app import gen_shell_completion
-    gen_shell_completion()
-
-
-class InstallWrapper(install):
-    def run(self):
-        install.run(self)
-        self.execute(_post_install, (self.install_lib,),
-                     msg="Installing shell completion")
-
-
-class DevelopWrapper(develop):
-    def run(self):
-        develop.run(self)
-        _post_install(None)
-
-
-setup(
-    name='xeda',
-
-    # Versions should comply with PEP440.  For a discussion on single-sourcing
-    # the version across setup.py and the project code, see
-    # https://packaging.python.org/en/latest/single_source_version.html
-    # version='0.1.0',
-
-    version_config={
-        "version_style": {
-            "style": "semver",
-            "dirty": True,
-        },
-    },
-
-    description='Cross EDA Abstraction and Automation',
-    long_description='''Xeda `/ˈziːdə/` is a cross-platform, cross-EDA, cross-target abstraction and automation platform for digital hardware simulation and synthesis flows.
-Xeda can assists hardware developers during verification, evaluation, and deployment of RTL designs. Xeda supports tools and flows from multiple commercial and open-source electronic design automation suites.''',
-
-    # The project's main homepage.
-    url='https://github.com/XedaHQ/xeda',
-
-
-    # Author details
-    author='Kamyar Mohajerani',
-    author_email='kamyar@ieee.org',
-
-    license='Apache-2.0',
-    # https://www.apache.org/licenses/LICENSE-2.0.txt
-    # https://opensource.org/licenses/Apache-2.0
-
-    # See https://pypi.python.org/pypi?%3Aaction=list_classifiers
-    classifiers=[
-        #   3 - Alpha
-        #   4 - Beta
-        #   5 - Production/Stable
-        'Development Status :: 3 - Alpha',
-        'Intended Audience :: Science/Research',
-        'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)',
-        'License :: OSI Approved :: Apache Software License',
-        'Topic :: Utilities',
-        'Programming Language :: Python :: 3.6',
-        'Programming Language :: Python :: 3.7',
-        'Programming Language :: Python :: 3.8',
-        'Programming Language :: Python :: 3.9',
-    ],
-
-    keywords='EDA Electronic Design Automation Tool Synthesis Simulation Hardware Verilog VHDL FPGA ASIC',
-    packages=find_packages(),
-
-    # Alternatively, if you want to distribute just a my_module.py, uncomment
-    # this:
-    # py_modules=['xeda'],
-
-    python_requires='>=3.6, <4',
-
-    # List run-time dependencies here.  These will be installed by pip when
-    # your project is installed. For an analysis of "install_requires" vs pip's
-    # requirements files see:
-    # https://packaging.python.org/en/latest/requirements.html
-    install_requires=[
-        "jinja2>=2.11.2", "colored", "progress>=1.5", "coloredlogs>=14", "pebble>=4", "numpy>=1",
-        "toml>=0.10.2", "shtab>=1.3.4"
-    ],
-
-    setup_requires=['setuptools-vcs-version'],
-
-
-    package_data={"":
-                  [
-                      '*.xdc', '*.sdf', '*.sdc', '*.dse', '*.fdc', '*.ldc', '*.tcl', '*.json', '*.ys'
-                  ]
-                  },
-    include_package_data=True,
-
-    # To provide executable scripts, use entry points in preference to the
-    # "scripts" keyword. Entry points provide cross-platform support and allow
-    # pip to create the appropriate form of executable for the target platform.
-    entry_points={
-        'console_scripts': [
-            'xeda=xeda:cli.run_xeda',
-        ],
-    },
-    cmdclass=dict(install=InstallWrapper, develop=DevelopWrapper),
-)
diff --git a/src/xeda/__init__.py b/src/xeda/__init__.py
new file mode 100644
index 00000000..59beecea
--- /dev/null
+++ b/src/xeda/__init__.py
@@ -0,0 +1,17 @@
+from . import cli, design, flows, flow_runner
+from .version import __version__
+from .design import Design
+from .flows.flow import Flow, SimFlow, SynthFlow, FPGA
+
+__all__ = [
+    "__version__",
+    "cli",
+    "design",
+    "flows",
+    "flow_runner",
+    "Design",
+    "Flow",
+    "SimFlow",
+    "SynthFlow",
+    "FPGA",
+]
diff --git a/src/xeda/__main__.py b/src/xeda/__main__.py
new file mode 100644
index 00000000..b6d23f7c
--- /dev/null
+++ b/src/xeda/__main__.py
@@ -0,0 +1,4 @@
+from . import cli
+
+if __name__ == "__main__":
+    cli.run()  # pylint: disable = no-value-for-parameter
diff --git a/src/xeda/board.py b/src/xeda/board.py
new file mode 100644
index 00000000..791eff44
--- /dev/null
+++ b/src/xeda/board.py
@@ -0,0 +1,70 @@
+import logging
+from typing import (
+    Any,
+    Dict,
+    Union,
+    Optional,
+)
+import pkg_resources
+import os
+
+from .utils import toml_loads, toml_load
+from .dataclass import root_validator
+from .flows.flow import FpgaSynthFlow, FPGA
+
+
+__all__ = [
+    "get_board_file_path",
+    "get_board_data",
+    "WithFpgaBoardSettings",
+]
+
+log = logging.getLogger(__name__)
+
+
+def get_board_file_path(file: str):
+    return pkg_resources.resource_filename("xeda.data.boards", file)
+
+
+def get_board_data(
+    board: Optional[str], custom_toml_file: Union[None, str, os.PathLike] = None
+) -> Optional[Dict[str, Any]]:
+    if not board:
+        return None
+    boards_data = {}
+    if custom_toml_file:
+        log.debug("Retrieving board data for %s from %s", board, custom_toml_file)
+        boards_data = toml_load(custom_toml_file)
+    else:
+        log.info("Retrieving board data for %s", board)
+        board_toml_bytes = pkg_resources.resource_string("xeda.data", "boards.toml")
+        if board_toml_bytes:
+            boards_data = toml_loads(board_toml_bytes.decode("utf-8"))
+        else:
+            log.error(
+                "Unable to get resource %s.%s. Please check xeda installation.",
+                "xeda.data",
+                "boards.toml",
+            )
+    return boards_data.get(board)
+
+
+class WithFpgaBoardSettings(FpgaSynthFlow.Settings):
+    board: Optional[str] = None
+    custom_boards_file: Optional[str] = None
+
+    @root_validator(pre=True)
+    def _fpga_validate(cls, values: Dict[str, Any]) -> Dict[str, Any]:
+        board_name = values.get("board")
+        log.debug("_fpga_validate! board_name=%s", board_name)
+        fpga = values.get("fpga")
+        if not fpga and board_name:
+            board_data = get_board_data(board_name)
+            if board_data:
+                board_fpga = board_data.get("fpga")
+                log.info("FPGA info for board %s: %s", board_name, str(board_fpga))
+                if board_fpga:
+                    if isinstance(board_fpga, str):
+                        board_fpga = {"part": board_fpga}
+                    values["fpga"] = FPGA(**board_fpga)
+        return values
diff --git a/src/xeda/cli.py b/src/xeda/cli.py
new file mode 100644
index 00000000..27f5ad5d
--- /dev/null
+++ b/src/xeda/cli.py
@@ -0,0 +1,467 @@
+# © 2022 [Kamyar Mohajerani](mailto:kamyar@ieee.org)
+"""Xeda Command-line interface"""
+from __future__ import annotations
+
+import inspect
+import logging
+import multiprocessing
+import os
+import sys
+from datetime import datetime
+from pathlib import Path
+from typing import Optional, Tuple
+
+import click
+import coloredlogs
+from click.shell_completion import get_completion_class
+from rich import box
+from rich.style import Style
+from rich.table import Table
+from simple_term_menu import TerminalMenu
+from typeguard.importhook import install_import_hook
+
+from .cli_utils import (
+    ClickMutex,
+    OptionEatAll,
+    XedaHelpGroup,
+    XedaOptions,
+    discover_flow_class,
+    print_flow_settings,
+    settings_to_dict,
+)
+from .console import console
+from .design import Design, DesignValidationError
+from .flow_runner import DefaultRunner
+from .flows.flow import (
+    Flow,
+    FlowException,
+    FlowFatalError,
+    FlowSettingsError,
+    registered_flows,
+)
+from .tool import ExecutableNotFound, NonZeroExitCode
+from .xedaproject import XedaProject
+
+install_import_hook("xeda")
+
+log = logging.getLogger(__name__)
+
+
+def load_design_from_toml(design_file) -> Design:
+    return Design.from_toml(design_file)
+
+
+def get_available_flows():
+    """alternative method using inspect"""
+    mod = "xeda.flows"
+    fc = inspect.getmembers(
+        sys.modules[mod],
+        lambda cls: inspect.isclass(cls)
+        and issubclass(cls, Flow)
+        and not inspect.isabstract(cls),
+    )
+    return {n: (mod, cls) for n, cls in fc}
+
+
+CONTEXT_SETTINGS = dict(
+    auto_envvar_prefix="XEDA",
+    help_option_names=["--help", "-h"],
+    max_content_width=console.width,
+)
+
+
+def log_to_file(logdir: Path):
+    timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S%f")[:-3]
+    logdir.mkdir(exist_ok=True, parents=True)
+    logfile = logdir / f"xeda_{timestamp}.log"
+    log.info("Logging to %s", logfile)
+    fileHandler = logging.FileHandler(logfile)
+    logFormatter = logging.Formatter(
+        "%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s]  %(message)s"
+    )
+    fileHandler.setFormatter(logFormatter)
+    log.root.addHandler(fileHandler)
+
+
+@click.group(cls=XedaHelpGroup, no_args_is_help=True, context_settings=CONTEXT_SETTINGS)
+@click.option("--verbose", is_flag=True, help="Enables verbose mode.")
+@click.option("--quiet", is_flag=True, help="Enable quiet mode.")
+@click.option("--debug", show_envvar=True, is_flag=True)
+@click.version_option(message="Xeda v%(version)s")
+@click.pass_context
+def cli(ctx: click.Context, **kwargs):
+    ctx.obj = XedaOptions(**kwargs)
+    log_level = (
+        logging.WARNING
+        if ctx.obj.quiet
+        else logging.DEBUG
+        if ctx.obj.debug
+        else logging.INFO
+    )
+    print("log_level:", logging.getLevelName(log_level))
+    log.root.setLevel(log_level)
+
+    coloredlogs.install(
+        None, fmt="%(asctime)s %(levelname)s %(message)s", logger=log.root
+    )
+
+
+@cli.command(
+    context_settings=CONTEXT_SETTINGS,
+    short_help="Run a flow.",
+    help="Run the flow identified by FLOW_NAME. A snake_case styled FLOW_NAME (e.g. ghdl_sim) is converted to a CamelCase class name (e.g. GhdlSim).",
+    no_args_is_help=False,
+)
+@click.argument(
+    "flow", metavar="FLOW_NAME", type=click.Choice(sorted(list(registered_flows)))
+)
+@click.option(
+    "--xeda-run-dir",
+    type=click.Path(
+        file_okay=False,
+        dir_okay=True,
+        writable=True,
+        readable=True,
+        resolve_path=True,
+        allow_dash=True,
+        path_type=Path,
+    ),
+    envvar="XEDA_RUN_DIR",
+    help="Parent folder for execution of xeda commands.",
+    default="xeda_run",
+    show_default=True,
+    show_envvar=True,
+)
+@click.option(
+    "--cached-dependencies",
+    is_flag=True,
+    help="Don't run dependency flows if a previous successfull run on the same design and flow settings exists.",
+)
+@click.option(
+    "--run_in_existing_dir",
+    is_flag=True,
+    help="DO NOT USE!",
+    hidden=True,
+)
+# @click.option(
+#     "--force-run",
+#     is_flag=True,
+#     help="Force re-run of flow and all dependencies, even if they are already up-to-date",
+# )
+@click.option(
+    "--xedaproject",
+    type=click.Path(
+        exists=True,
+        file_okay=True,
+        dir_okay=False,
+        writable=False,
+        readable=True,
+        resolve_path=True,
+        allow_dash=False,
+        path_type=Path,
+    ),
+    cls=ClickMutex,
+    mutually_exclusive_with=["design_file"],
+    help="Path to Xeda project file.",
+)
+@click.option(
+    "--design-name",
+    cls=ClickMutex,
+    mutually_exclusive_with=["design_file"],
+    help="Specify design.name in case multiple designs are available in a xedaproject.",
+)
+@click.option(
+    "--design-file",
+    "--design",
+    type=click.Path(
+        exists=True,
+        file_okay=True,
+        dir_okay=False,
+        writable=False,
+        readable=True,
+        resolve_path=True,
+        allow_dash=False,
+        path_type=Path,
+    ),
+    cls=ClickMutex,
+    mutually_exclusive_with=["xedaproject"],
+    help="Path to Xeda design file containing the description of a single design.",
+)
+@click.option(
+    "--flow-settings",
+    "--settings",
+    metavar="KEY=VALUE...",
+    type=tuple,
+    cls=OptionEatAll,
+    help="""Override setting values for the executed flow. Separate multiple KEY=VALUE overrides with commas. KEY can be a hierarchical name using dot notation.
+    Example: --settings clock_period=2.345 impl.strategy=Debug
+    """
+    #  examples: # FIXME move to docs
+    # - xeda vivado_sim --flow-settings stop_time=100us
+    # - xeda vivado_synth --flow-settings impl.strategy=Debug --flow-settings clock_period=2.345
+)  # pylint: disable=C0116:missing-function-docstring
+@click.pass_context
+def run(
+    ctx: click.Context,
+    flow: str,
+    cached_dependencies: bool = False,
+    run_in_existing_dir: bool = False,
+    # force_run: bool = False,
+    xeda_run_dir: Optional[Path] = None,
+    xedaproject: Optional[str] = None,
+    design_name: Optional[str] = None,
+    design_file: Optional[str] = None,
+    flow_settings: Optional[Tuple[str, ...]] = None,
+):
+    assert ctx
+    options: XedaOptions = ctx.obj or XedaOptions()
+    assert xeda_run_dir
+    log_to_file(xeda_run_dir / "Logs")
+    # TODO get default flow configs?
+    flows_config = {}
+    if design_file:
+        try:
+            design = Design.from_toml(design_file)
+        except DesignValidationError as e:
+            log.critical("%s", e)
+            sys.exit(1)
+    else:
+        if not xedaproject:
+            xedaproject = "xedaproject.toml"
+            if not Path(xedaproject).exists():
+                sys.exit(
+                    "No design file or project files were specified and no `xedaproject.toml` was found in the working directory."
+                )
+        try:
+            xeda_project = XedaProject.from_file(xedaproject)
+        except DesignValidationError as e:
+            log.critical("%s", e)
+            sys.exit(1)
+        except FileNotFoundError:
+            sys.exit(
+                f"Cannot open project file: {xedaproject}. Please run from the project directory with xedaproject.toml or specify the correct path using the --xedaproject flag"
+            )
+        flows_config = xeda_project.flows
+        designs = xeda_project.designs
+        assert isinstance(xeda_project.design_names, list)  # type checker
+        log.info(
+            "Available designs in xedaproject: %s",
+            ", ".join(xeda_project.design_names),
+        )
+        design = xeda_project.get_design(design_name)
+        if design_name:
+            if not design:
+                log.critical(
+                    'Design "%s" not found in %s. Available designs are: %s',
+                    design_name,
+                    xedaproject,
+                    ", ".join(xeda_project.design_names),
+                )
+                sys.exit(1)
+        else:
+            if console.is_interactive:
+                terminal_menu = TerminalMenu(
+                    xeda_project.design_names, title="Please select a design: "
+                )
+                idx = terminal_menu.show()
+                if idx is None or not isinstance(idx, int) or idx < 0:
+                    sys.exit("Invalid design choice!")
+                design = designs[idx]
+            else:
+                design_name = click.prompt(
+                    "Please enter design name: ",
+                    type=click.Choice(xeda_project.design_names),
+                )
+                if not design_name or design_name not in xeda_project.design_names:
+                    sys.exit("Invalid design name!")
+                design = xeda_project.get_design(design_name)
+        if not design:
+            sys.exit("[ERROR] design is empty?!")
+
+    flow_overrides = settings_to_dict(flow_settings)
+    log.debug("flow_overrides: %s", flow_overrides)
+    flow_overrides = {**flows_config.get(flow, {}), **flow_overrides}
+    flow_class = discover_flow_class(flow)
+    runner = DefaultRunner(
+        xeda_run_dir,
+        debug=options.debug,
+        cached_dependencies=cached_dependencies,
+        run_in_existing_dir=run_in_existing_dir,
+    )
+    try:
+        runner.run_flow(flow_class, design, flow_overrides)
+    except FlowFatalError as e:
+        log.critical(
+            "Flow %s failed: FlowFatalException %s",
+            flow,
+            " ".join(str(a) for a in e.args),
+        )
+        sys.exit(1)
+    except NonZeroExitCode as e:
+        log.critical(
+            "Flow %s failed: NonZeroExitCode %s", flow, " ".join(str(a) for a in e.args)
+        )
+        sys.exit(1)
+    except ExecutableNotFound as e:
+        log.critical(
+            "Executable '%s' was not found! (tool:%s, flow:%s, PATH:%s)",
+            e.exec,
+            e.tool,
+            flow,
+            e.path,
+        )
+        sys.exit(1)
+    except FlowSettingsError as e:
+        log.critical("%s", e)
+        if options.debug:
+            raise e from None
+        sys.exit(1)
+    except FlowException as e:  # any flow exception
+        log.critical("%s", e)
+        if options.debug:
+            raise e from None
+        sys.exit(1)
+
+
+@cli.command(context_settings=CONTEXT_SETTINGS, short_help="List available flows.")
+def list_flows():
+    table = Table(
+        title="Available flows",
+        show_header=True,
+        header_style="bold yellow",
+        title_style=Style(frame=True, bold=True),
+        box=box.HEAVY_HEAD,
+        show_lines=True,
+    )
+    table.add_column("Flow", header_style="bold green", style="bold")
+    table.add_column("Description")
+    table.add_column("Class", style="dim")
+    super_flow_doc = inspect.getdoc(Flow)
+    for cls_name, (_mod, cls) in sorted(registered_flows.items()):
+        doc = inspect.getdoc(cls)
+        if doc == super_flow_doc:
+            doc = "<no description>"
+        table.add_row(
+            cls_name,
+            doc,
+            str(cls.__module__) + "." + cls.__name__,
+        )
+    console.print(table)
+
+
+@cli.command(
+    context_settings=CONTEXT_SETTINGS, short_help="List flow settings information"
+)
+@click.argument(
+    "flow",
+    metavar="FLOW_NAME",
+    type=click.Choice(sorted(list(registered_flows))),
+    required=True,
+)
+@click.pass_context
+def list_settings(ctx: click.Context, flow):
+    print_flow_settings(flow, options=ctx.obj)
+
+
+@cli.command(
+    context_settings=CONTEXT_SETTINGS,
+    short_help="Design-space exploration: run several instances of a flow to find optimal parameters and results",
+)
+@click.argument("flow")
+@click.option(
+    "--max-cpus",
+    default=max(1, multiprocessing.cpu_count()),
+    type=int,
+    help="Maximum total number of logical CPU cores to use.",
+    show_default=True,
+    show_envvar=True,
+)
+@click.pass_context
+def dse(ctx, flow, max_cpus):  # pylint: disable=unused-argument
+    """Design-space exploration (e.g. fmax)"""
+
+
+SHELLS = {
+    "bash": {
+        "eval_file": "~/.bashrc",
+        "eval": 'eval "$(_XEDA_COMPLETE=bash_source xeda)"',
+        "completion_file": None,
+    },
+    "zsh": {
+        "eval_file": "~/.zshrc",
+        "eval": 'eval "$(_XEDA_COMPLETE=zsh_source xeda)"',
+        "completion_file": None,
+    },
+    "fish": {
+        "eval_file": "~/.config/fish/completions/xeda.fish",
+        "eval": "eval (env _XEDA_COMPLETE=fish_source xeda)",
+        "completion_file": None,
+    },
+}
+
+
+@cli.command(
+    context_settings=CONTEXT_SETTINGS,
+    short_help="Shell completion",
+    help=f"Name of the shell. Supported shells are: {', '.join(SHELLS.keys())}",
+)
+@click.argument(
+    "shell",
+    required=False,
+    type=click.Choice(list(SHELLS.keys()), case_sensitive=False),
+)
+@click.option(
+    "--stdout",
+    is_flag=True,
+    cls=ClickMutex,
+    help="""Produce the shell completion script and output it to the standard output.\n
+    Example usage:\n
+         $ xeda completion zsh --stdout  > $HOME/.zsh/completion/_xeda\n
+         $ xeda completion bash --stdout > $HOME/.local/share/bash-completion/xeda
+    """,
+)
+@click.pass_context
+def completion(_ctx: click.Context, stdout, shell=None):
+    """Xeda shell auto-completion"""
+    os_default_shell_name = None
+    os_default_shell = os.environ.get("SHELL")
+    if os_default_shell:
+        os_default_shell_name = os_default_shell.split(os.sep)[-1]
+    if os_default_shell_name:
+        if not shell:
+            shell = os_default_shell_name
+        elif os_default_shell_name != shell and not stdout:
+            console.print(
+                f"[yellow]WARNING:[/] Current default shell ([bold]{os_default_shell}[/]) is different from the specified shell [bold]{shell}[/b]"
+            )
+    if stdout:
+        completion_class = get_completion_class(shell)
+        if completion_class:
+            complete = completion_class(
+                cli=cli, ctx_args={}, prog_name=__package__, complete_var="source_xeda"
+            )
+            print(complete.source())
+    else:
+        console.print(
+            f"""
+    Make sure 'xeda' executable is properly installed and is accessible through the shell's PATH.
+        [yellow]$ xeda --version[/]
+    Add the following line to [magenta underline]{SHELLS[shell]['eval_file']}[/]:
+        {SHELLS[shell]['eval']}
+            """,
+            highlight=False,
+        )
+
+
+@cli.command()
+@click.argument("subcommand", required=False)
+@click.pass_context
+def help(ctx: click.Context, subcommand=None):  # pylint: disable=redefined-builtin
+    if subcommand:
+        subcommand_obj = cli.get_command(ctx, subcommand)
+        if subcommand_obj is None:
+            click.echo("I don't know that command.")
+        else:
+            click.echo(subcommand_obj.get_help(ctx))
+    else:
+        click.echo(cli.get_usage(ctx))
diff --git a/src/xeda/cli_utils.py b/src/xeda/cli_utils.py
new file mode 100644
index 00000000..029ea703
--- /dev/null
+++ b/src/xeda/cli_utils.py
@@ -0,0 +1,341 @@
+"""Utilities for command line interface"""
+import logging
+import re
+import sys
+from dataclasses import dataclass
+from functools import reduce
+from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
+
+import click
+from click_help_colors import HelpColorsGroup
+from overrides import overrides
+from rich import box
+from rich.style import Style
+from rich.table import Table
+from rich.text import Text
+
+from .console import console
+from .dataclass import asdict
+from .flow_runner import FlowNotFoundError, get_flow_class
+from .flows.flow import Flow
+from .utils import set_hierarchy, try_convert
+
+__all__ = [
+    "ClickMutex",
+    "OptionEatAll",
+    "ConsoleLogo",
+    "XedaHelpGroup",
+    "discover_flow_class",
+]
+
+
+log = logging.getLogger(__name__)
+
+
+@dataclass
+class XedaOptions:
+    verbose: bool = False
+    quiet: bool = False
+    debug: bool = False
+
+
+class ClickMutex(click.Option):
+    """Mutual exclusion of options"""
+
+    def __init__(self, *args, **kwargs):
+        self.mutually_exclusive_with: list = kwargs.pop("mutually_exclusive_with", [])
+        self.required_if: list = kwargs.pop("required_if", [])
+        if self.mutually_exclusive_with:
+            kwargs["help"] = (
+                kwargs.get("help", "")
+                + "Option is mutually exclusive with "
+                + ", ".join(self.mutually_exclusive_with)
+                + "."
+            ).strip()
+        if self.required_if:
+            kwargs["help"] = (
+                kwargs.get("help", "")
+                + "Option is required only if "
+                + " or ".join(self.mutually_exclusive_with)
+                + " is specified "
+            ).strip()
+        super().__init__(*args, **kwargs)
+
+    def handle_parse_result(self, ctx, opts, args):
+        if self.name in opts:
+            for mutex_opt in self.mutually_exclusive_with:
+                if mutex_opt in opts:
+                    raise click.UsageError(
+                        f"Option {self.name} is mutually exclusive with {mutex_opt}."
+                    )
+        else:
+            for dependent_opt in self.required_if:
+                if dependent_opt in opts:
+                    raise click.UsageError(
+                        f"Option {self.name} is required when {dependent_opt} is specified."
+                    )
+
+        return super().handle_parse_result(ctx, opts, args)
+
+
+class OptionEatAll(click.Option):
+    """
+    Taken from https://stackoverflow.com/questions/48391777/nargs-equivalent-for-options-in-click#answer-48394004.
+    """  # noqa: ignore=E501 pylint: disable=C0301
+
+    @overrides
+    def __init__(self, *args, **kwargs):
+        self.save_other_options = kwargs.pop("save_other_options", True)
+        super().__init__(*args, **kwargs)
+        self._previous_parser_process: Optional[
+            Callable[[Union[str, Tuple[str, ...]], click.parser.ParsingState], None]
+        ] = None
+        self._eat_all_parser = None
+
+    @overrides
+    def add_to_parser(self, parser, ctx):
+        def parser_process(value: str, state: click.parser.ParsingState):
+            """method to hook to the parser.process"""
+            value_list = [value]
+            if self._eat_all_parser is not None and self.save_other_options:
+                # grab everything up to the next option
+                done = False
+                while state.rargs:
+                    for prefix in self._eat_all_parser.prefixes:
+                        if state.rargs[0].startswith(prefix):
+                            done = True
+                            break
+                    if done:
+                        break
+                    value_list.append(state.rargs.pop(0))
+            else:
+                # grab everything remaining
+                value_list += state.rargs
+                state.rargs[:] = []
+
+            # call the actual process
+            if self._previous_parser_process is not None:
+                self._previous_parser_process(tuple(value_list), state)
+
+        super().add_to_parser(parser, ctx)
+        for name in self.opts:
+            # pylint: disable=protected-access
+            our_parser = parser._long_opt.get(name) or parser._short_opt.get(name)
+            if our_parser:
+                self._eat_all_parser = our_parser
+                self._previous_parser_process = our_parser.process  # type: ignore
+                our_parser.process = parser_process
+                break
+
+
+class ConsoleLogo:
+    """Xeda ASCII logo on console"""
+
+    logo: str = """
+:==-+-:        :=-.-='.---==:::::::==-. .--=:::::::::=.          ,+==::::::+=-.
+ .= {X}O{O} -=     .=--.==. *  ,...........:: *:  {X}........{O}  `=-      ,='            *
+   :=:--=-  =-..-=.   +  .-:::::::::::' +: {X}=+++++++++-{O} `:+   ,=' .*########:  *
+     -=.-:=-.-:=:     +  +:             +: {X}=+++++++++*.{O} `=- .=: :*########%:  *
+       -==-.-=-       +  +: ,:::::::-,  +: {X}=++++++++++*:{O} := =: :*%########%:  *
+      .==-.-==-       +  +: `:::::::='  +: {X}=++++++++++*:{O} := =: `*%#######%*'  *
+     -= -:==. ==-     +  +:             +: {X}=++++++++++*:{O} .= =:                *
+   := -==-  ==. ==-   *  `+..........-. +. {X}=+++++++++*:{O} .+  =: .==::::::::=.  *
+ .:-.==:     .=- {X}O{O} =. :=+-............: *, {X}:++++++++'{O} .+-   =: -=          +  *
+:=--==.        :=--.=: `=+:::::::::::-' `*-::::::::::+-'    `=-='          `-='
+    """
+
+    @classmethod
+    def print(cls):
+        """print the logo if console is wide enough"""
+        if console.width >= 80:
+            console.print(
+                cls.logo.format(X="[dark_orange]", O="[/]"),
+                highlight=False,
+                emoji=False,
+            )
+
+
+class XedaHelpGroup(HelpColorsGroup):
+    """How to display CLI help"""
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.help_headers_color = "yellow"
+        self.help_options_color = "green"
+
+    def format_usage(self, ctx: click.Context, formatter: click.HelpFormatter):
+        ConsoleLogo.print()
+        super().format_usage(ctx, formatter)
+
+
+# DictStrHier = Dict[str, "StrOrDictStrHier"]
+DictStrHier = Dict[str, Any]
+StrOrDictStrHier = Union[str, DictStrHier]
+
+
+def settings_to_dict(
+    settings: Union[
+        None, List[str], Tuple[str, ...], Dict[str, StrOrDictStrHier], Flow.Settings
+    ],
+) -> Dict[str, Any]:
+    if not settings:
+        return {}
+    if isinstance(settings, (tuple, list)):
+        res: DictStrHier = {}
+        for override in settings:
+            sp = override.split("=")
+            if len(sp) != 2:
+                raise ValueError("Settings should be in KEY=VALUE format!")
+            key, val = sp
+            set_hierarchy(res, key, try_convert(val, convert_lists=True))
+        return res
+    if isinstance(settings, Flow.Settings):
+        return asdict(settings)
+    if isinstance(settings, dict):
+        return settings
+    raise TypeError(f"overrides is of unsupported type: {type(settings)}")
+
+
+def print_flow_settings(flow, options: XedaOptions):
+    flow_class = discover_flow_class(flow)
+    schema = flow_class.Settings.schema(by_alias=True)
+    type_defs = {}
+
+    def get_type(field):
+        if isinstance(field, (list, tuple)):
+            return Text("|".join(str(get_type(f)) for f in field))
+        typ = field.get("type")
+        ref = field.get("$ref")
+        if typ is None and ref:
+            typ = ref.split("/")[-1]
+            typ_def = schema.get("definitions", {}).get(typ)
+            if typ not in type_defs:
+                type_defs[typ] = typ_def
+            elif type_defs[typ] != typ_def:
+                log.critical(
+                    "type definition for %s changed!\nPrevious def:\n %s new def:\n %s",
+                    typ,
+                    type_defs[typ],
+                    typ_def,
+                )
+            return Text(typ, style="blue")
+        additional = field.get("additionalProperties")
+        if typ == "object" and additional:
+            return Text(f"Dict[string -> {get_type(additional)}]")
+
+        if typ == "array":
+            items = field.get("items")
+            if items:
+                return Text(f"array[{get_type(items)}]")
+
+        def join_types(lst, joiner, style="red"):
+            return reduce(
+                lambda x, y: x + Text(joiner, style) + y, (get_type(t) for t in lst)
+            )
+
+        allof = field.get("allOf")
+        if allof:
+            return join_types(allof, " & ")  # intersection
+        anyOf = field.get("anyOf")
+        if anyOf:
+            return join_types(anyOf, " | ")  # union
+        return Text(typ)
+
+    table = Table(
+        title=f"{flow} settings",
+        show_header=True,
+        header_style="bold yellow",
+        title_style=Style(frame=True, bold=True),
+        box=box.HEAVY_HEAD,
+        show_lines=True,
+    )
+    table.add_column("Property", header_style="bold green", style="bold")
+    table.add_column("Type", max_width=32)
+    table.add_column("Default", max_width=42)
+    table.add_column("Description")
+
+    def fmt_default(v: Any) -> str:
+        if isinstance(v, str):
+            return f'"{v}"'
+        if isinstance(v, bool):
+            return str(v).lower()
+        return str(v)
+
+    for name, field in schema.get("properties", {}).items():
+        if name in ["results"] or name in Flow.Settings.__fields__.keys():
+            continue
+        required = name in schema.get("required", [])
+        desc: str = field.get("description", "")
+        typ = get_type(field)
+        req_or_def = (
+            "[red]<required>[/red]" if required else fmt_default(field.get("default"))
+        )
+        table.add_row(name, typ, req_or_def, desc)
+    console.print(table)
+
+    for typ_name, typ_def in list(type_defs.items()):
+        for prop, prop_def in typ_def.get("properties").items():
+            get_type(prop_def)
+    for typ_name, typ_def in list(type_defs.items()):
+        c = "blue"
+        table = Table(
+            title=f"Type [{c}]{typ_name}[/{c}]",
+            show_header=True,
+            header_style="bold green",
+            title_style=Style(frame=True, bold=True),
+            box=box.SQUARE_DOUBLE_HEAD,
+            show_lines=True,
+        )
+        if options.debug:
+            console.print(f"Type: {typ_name}")
+            console.print_json(data=typ_def)
+        table.add_column("property")
+        table.add_column("type")
+        table.add_column("description")
+        for prop, prop_def in typ_def.get("properties").items():
+            if prop_def.get("hidden_from_schema"):
+                continue
+            if (
+                typ_name.endswith("__Settings")
+                and prop in Flow.Settings.__fields__.keys()
+            ):
+                continue
+            desc = prop_def.get("description", prop_def.get("title", "-"))
+            desc = re.sub(r"\s*\.*\s*$", "", desc)
+            table.add_row(prop, get_type(prop_def), desc)
+        console.print(table)
+
+
+# xdg_home = os.environ.get('XDG_DATA_HOME', os.path.join(
+#     os.environ.get('HOME', str(Path.home())), '.local', 'share'))
+# completion_user_dir = os.environ.get(
+#     'BASH_COMPLETION_USER_DIR', os.path.join(xdg_home, 'bash-completion'))
+# dir = Path(completion_user_dir)
+# if not dir.exists():
+#     dir.mkdir(parents=True)
+# completion_file = dir / 'xeda'
+# with open(completion_file, 'w') as f:
+#     f.write(completion)
+# eager_completion = Path.home() / '.bash_completion'
+# source_line = f'. {completion_file} # added by xeda'
+# if eager_completion.exists():
+#     with open(eager_completion, "r+") as f:
+#         for line in f:
+#             if source_line in line:
+#                 break
+#         else:  # else-for "completion clause"
+#             f.write(source_line + os.linesep)
+# else:
+#     with open(eager_completion, "w") as f:
+#         f.write(source_line + os.linesep)
+
+
+def discover_flow_class(flow: str) -> Type[Flow]:
+    try:
+        return get_flow_class(flow, "xeda.flows", __package__)
+    except FlowNotFoundError:
+        log.critical(
+            "Flow %s is not known to Xeda. Please make sure the name is correctly specified.",
+            flow,
+        )
+        sys.exit(1)
diff --git a/src/xeda/console.py b/src/xeda/console.py
new file mode 100644
index 00000000..a9463afd
--- /dev/null
+++ b/src/xeda/console.py
@@ -0,0 +1,3 @@
+from rich.console import Console
+
+console = Console()
diff --git a/xeda/data/boards/__init__.py b/src/xeda/data/__init__.py
similarity index 100%
rename from xeda/data/boards/__init__.py
rename to src/xeda/data/__init__.py
diff --git a/src/xeda/data/boards.toml b/src/xeda/data/boards.toml
new file mode 100644
index 00000000..6644d6ae
--- /dev/null
+++ b/src/xeda/data/boards.toml
@@ -0,0 +1,5 @@
+## board name _ variant _ version (no dots or otherwise surround with double quotes)
+[ULX3S_85F]
+name = 'ulx3s'
+fpga.part = 'LFE5U-85F-6BG381C'
+lpf = 'https://raw.githubusercontent.com/emard/ulx3s/master/doc/constraints/ulx3s_v20.lpf'
diff --git a/xeda/data/boards/ulx3s/__init__.py b/src/xeda/data/boards/__init__.py
similarity index 100%
rename from xeda/data/boards/ulx3s/__init__.py
rename to src/xeda/data/boards/__init__.py
diff --git a/xeda/flows/dc/templates/__init__.py b/src/xeda/data/boards/ulx3s/__init__.py
similarity index 100%
rename from xeda/flows/dc/templates/__init__.py
rename to src/xeda/data/boards/ulx3s/__init__.py
diff --git a/src/xeda/data/boards/ulx3s/board.lpf b/src/xeda/data/boards/ulx3s/board.lpf
new file mode 100644
index 00000000..49d246e0
--- /dev/null
+++ b/src/xeda/data/boards/ulx3s/board.lpf
@@ -0,0 +1,598 @@
+BLOCK RESETPATHS;
+BLOCK ASYNCPATHS;
+## ULX3S v2.x.x and v3.0.x
+
+# The clock "usb" and "gpdi" sheet
+LOCATE COMP "clk_25mhz" SITE "G2";
+IOBUF  PORT "clk_25mhz" PULLMODE=NONE IO_TYPE=LVCMOS33;
+FREQUENCY PORT "clk_25mhz" 25 MHZ;
+
+# JTAG and SPI FLASH voltage 3.3V and options to boot from SPI flash
+# write to FLASH possible any time from JTAG:
+SYSCONFIG CONFIG_IOVOLTAGE=3.3 COMPRESS_CONFIG=ON MCCLK_FREQ=62 SLAVE_SPI_PORT=DISABLE MASTER_SPI_PORT=ENABLE SLAVE_PARALLEL_PORT=DISABLE;
+# write to FLASH possible from user bitstream:
+# SYSCONFIG CONFIG_IOVOLTAGE=3.3 COMPRESS_CONFIG=ON MCCLK_FREQ=62 SLAVE_SPI_PORT=DISABLE MASTER_SPI_PORT=DISABLE SLAVE_PARALLEL_PORT=DISABLE;
+
+## USBSERIAL FTDI-FPGA serial port "usb" sheet
+LOCATE COMP "ftdi_rxd" SITE "L4"; # FPGA transmits to ftdi
+LOCATE COMP "ftdi_txd" SITE "M1"; # FPGA receives from ftdi
+LOCATE COMP "ftdi_nrts" SITE "M3"; # FPGA receives
+LOCATE COMP "ftdi_ndtr" SITE "N1"; # FPGA receives
+LOCATE COMP "ftdi_txden" SITE "L3"; # FPGA receives
+IOBUF  PORT "ftdi_rxd" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "ftdi_txd" PULLMODE=UP IO_TYPE=LVCMOS33;
+IOBUF  PORT "ftdi_nrts" PULLMODE=UP IO_TYPE=LVCMOS33;
+IOBUF  PORT "ftdi_ndtr" PULLMODE=UP IO_TYPE=LVCMOS33;
+IOBUF  PORT "ftdi_txden" PULLMODE=UP IO_TYPE=LVCMOS33;
+
+## LED indicators "blinkey" and "gpio" sheet
+LOCATE COMP "led[7]" SITE "H3";
+LOCATE COMP "led[6]" SITE "E1";
+LOCATE COMP "led[5]" SITE "E2";
+LOCATE COMP "led[4]" SITE "D1";
+LOCATE COMP "led[3]" SITE "D2";
+LOCATE COMP "led[2]" SITE "C1";
+LOCATE COMP "led[1]" SITE "C2";
+LOCATE COMP "led[0]" SITE "B2";
+IOBUF  PORT "led[0]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "led[1]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "led[2]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "led[3]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "led[4]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "led[5]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "led[6]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "led[7]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+
+## Pushbuttons "blinkey", "flash", "power", "gpdi" sheet
+LOCATE COMP "btn[0]" SITE "D6";  # BTN_PWRn (inverted logic)
+LOCATE COMP "btn[1]" SITE "R1";  # FIRE1
+LOCATE COMP "btn[2]" SITE "T1";  # FIRE2
+LOCATE COMP "btn[3]" SITE "R18"; # UP W1->R18
+LOCATE COMP "btn[4]" SITE "V1";  # DOWN
+LOCATE COMP "btn[5]" SITE "U1";  # LEFT
+LOCATE COMP "btn[6]" SITE "H16"; # RIGHT Y2->H16
+IOBUF  PORT "btn[0]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "btn[1]" PULLMODE=DOWN IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "btn[2]" PULLMODE=DOWN IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "btn[3]" PULLMODE=DOWN IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "btn[4]" PULLMODE=DOWN IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "btn[5]" PULLMODE=DOWN IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "btn[6]" PULLMODE=DOWN IO_TYPE=LVCMOS33 DRIVE=4;
+
+## DIP switch "blinkey", "gpio" sheet
+LOCATE COMP "sw[0]" SITE "E8"; # SW1
+LOCATE COMP "sw[1]" SITE "D8"; # SW2
+LOCATE COMP "sw[2]" SITE "D7"; # SW3
+LOCATE COMP "sw[3]" SITE "E7"; # SW4
+IOBUF  PORT "sw[0]" PULLMODE=DOWN IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "sw[1]" PULLMODE=DOWN IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "sw[2]" PULLMODE=DOWN IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "sw[3]" PULLMODE=DOWN IO_TYPE=LVCMOS33 DRIVE=4;
+
+## SPI OLED DISPLAY SSD1331 (Color) or SSD1306 (B/W) "blinkey", "usb" sheet
+LOCATE COMP "oled_clk" SITE "P4";
+LOCATE COMP "oled_mosi" SITE "P3";
+LOCATE COMP "oled_dc" SITE "P1";
+LOCATE COMP "oled_resn" SITE "P2";
+LOCATE COMP "oled_csn" SITE "N2";
+IOBUF  PORT "oled_clk" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "oled_mosi" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "oled_dc" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "oled_resn" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "oled_csn" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+
+## SPI Flash chip "flash" sheet
+LOCATE COMP "flash_csn" SITE "R2";
+LOCATE COMP "flash_clk" SITE "U3";
+LOCATE COMP "flash_mosi" SITE "W2";
+LOCATE COMP "flash_miso" SITE "V2";
+LOCATE COMP "flash_holdn" SITE "W1";
+LOCATE COMP "flash_wpn" SITE "Y2";
+#LOCATE COMP "flash_csspin" SITE "AJ3";
+#LOCATE COMP "flash_initn" SITE "AG4";
+#LOCATE COMP "flash_done" SITE "AJ4";
+#LOCATE COMP "flash_programn" SITE "AH4";
+#LOCATE COMP "flash_cfg_select[0]" SITE "AM4";
+#LOCATE COMP "flash_cfg_select[1]" SITE "AL4";
+#LOCATE COMP "flash_cfg_select[2]" SITE "AK4";
+IOBUF  PORT "flash_csn" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "flash_clk" PULLMODE=DOWN IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "flash_mosi" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "flash_miso" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "flash_holdn" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "flash_wpn" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+#IOBUF  PORT "flash_csspin" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+#IOBUF  PORT "flash_initn" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+#IOBUF  PORT "flash_done" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+#IOBUF  PORT "flash_programn" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+#IOBUF  PORT "flash_cfg_select[0]" PULLMODE=DOWN IO_TYPE=LVCMOS33 DRIVE=4;
+#IOBUF  PORT "flash_cfg_select[1]" PULLMODE=DOWN IO_TYPE=LVCMOS33 DRIVE=4;
+#IOBUF  PORT "flash_cfg_select[2]" PULLMODE=DOWN IO_TYPE=LVCMOS33 DRIVE=4;
+
+## SD card "sdcard", "usb" sheet
+# wifi_gpio2,4,12,13,14,15 are shared with SD card.
+# If any of wifi_gpio2,4,12,13 is used in toplevel, don't use sd_d[].
+# If SD is used in 1-bit SPI mode, wifi_gpio4,12 = sd_d[1,2] are free,
+LOCATE COMP "sd_clk" SITE "H2"; # sd_clk WiFi_GPIO14
+LOCATE COMP "sd_cmd" SITE "J1"; # sd_cmd_di (MOSI) WiFi GPIO15
+LOCATE COMP "sd_d[0]" SITE "J3"; # sd_d0_do (MISO) WiFi GPIO2
+LOCATE COMP "sd_d[1]" SITE "H1"; # sd_d1_irq WiFi GPIO4
+LOCATE COMP "sd_d[2]" SITE "K1"; # sd_d2 WiFi_GPIO12
+LOCATE COMP "sd_d[3]" SITE "K2"; # sd_d3_csn WiFi_GPIO13
+LOCATE COMP "sd_wp" SITE "P5"; # not connected
+LOCATE COMP "sd_cdn" SITE "N5"; # not connected
+IOBUF  PORT "sd_clk" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "sd_cmd" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "sd_d[0]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "sd_d[1]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "sd_d[2]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4; # WiFi GPIO12 pulldown bootstrapping without 3.3V efuse
+IOBUF  PORT "sd_d[3]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "sd_wp" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "sd_cdn" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+
+## ADC SPI (MAX11123) "analog", "ram" sheet
+# input lines shared with GP,GN14-17
+LOCATE COMP "adc_csn" SITE "R17";
+LOCATE COMP "adc_mosi" SITE "R16";
+LOCATE COMP "adc_miso" SITE "U16";
+LOCATE COMP "adc_sclk" SITE "P17";
+IOBUF  PORT "adc_csn" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "adc_mosi" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "adc_miso" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "adc_sclk" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+
+## Audio 4-bit DAC "analog", "gpio" sheet
+# output impedance: 75 ohm
+# Stereo 16 ohm earphones, analog audio,
+# SPDIF digital audio and composite video.
+LOCATE COMP "audio_l[3]" SITE "B3"; # JACK TIP (left audio)
+LOCATE COMP "audio_l[2]" SITE "C3";
+LOCATE COMP "audio_l[1]" SITE "D3";
+LOCATE COMP "audio_l[0]" SITE "E4";
+LOCATE COMP "audio_r[3]" SITE "C5"; # JACK RING1 (right audio)
+LOCATE COMP "audio_r[2]" SITE "D5";
+LOCATE COMP "audio_r[1]" SITE "B5";
+LOCATE COMP "audio_r[0]" SITE "A3";
+LOCATE COMP "audio_v[3]" SITE "E5"; # JACK RING2 (video or digital audio)
+LOCATE COMP "audio_v[2]" SITE "F5";
+LOCATE COMP "audio_v[1]" SITE "F2";
+LOCATE COMP "audio_v[0]" SITE "H5";
+IOBUF  PORT "audio_l[3]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
+IOBUF  PORT "audio_l[2]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
+IOBUF  PORT "audio_l[1]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
+IOBUF  PORT "audio_l[0]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
+IOBUF  PORT "audio_r[3]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
+IOBUF  PORT "audio_r[2]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
+IOBUF  PORT "audio_r[1]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
+IOBUF  PORT "audio_r[0]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
+IOBUF  PORT "audio_v[3]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
+IOBUF  PORT "audio_v[2]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
+IOBUF  PORT "audio_v[1]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
+IOBUF  PORT "audio_v[0]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
+
+## WiFi ESP-32 "wifi", "usb", "flash" sheet
+# wifi_gpio2,4,12,13,14,15 are shared with SD card.
+# If any of wifi_gpio2,4,12,13 is used in toplevel, don't use sd_d[].
+# If SD is used in 1-bit SPI mode, wifi_gpio4,12 = sd_d[1,2] are free,
+# other pins are shared with GP/GN, and JTAG
+LOCATE COMP "wifi_en" SITE "F1"; # enable/reset WiFi
+LOCATE COMP "wifi_rxd" SITE "K3"; # FPGA transmits to WiFi
+LOCATE COMP "wifi_txd" SITE "K4"; # FPGA receives from WiFi
+LOCATE COMP "wifi_gpio0" SITE "L2";
+LOCATE COMP "wifi_gpio5" SITE "N4"; # WIFI LED
+LOCATE COMP "wifi_gpio16" SITE "L1"; # Serial1 RX
+LOCATE COMP "wifi_gpio17" SITE "N3"; # Serial1 TX
+# LOCATE COMP "prog_done" SITE "Y3"; # not GPIO, always active
+# wifi lines shared with SD card
+LOCATE COMP "wifi_gpio2" SITE "J3"; # sd_d0_do (MISO) WiFi GPIO2
+LOCATE COMP "wifi_gpio4" SITE "H1"; # sd_d1_irq WiFi GPIO4
+LOCATE COMP "wifi_gpio12" SITE "K1"; # sd_d2 WiFi_GPIO12
+LOCATE COMP "wifi_gpio13" SITE "K2"; # sd_d3_csn WiFi_GPIO13
+LOCATE COMP "wifi_gpio14" SITE "H2"; # sd_clk WiFi_GPIO14
+LOCATE COMP "wifi_gpio15" SITE "J1"; # sd_cmd_di (MOSI) WiFi GPIO15
+# wifi lines shared with JTAG
+# LOCATE COMP "wifi_gpio21" SITE "U5"; # JTAG TMS
+# LOCATE COMP "wifi_gpio18" SITE "T5"; # JTAG TCK
+# LOCATE COMP "wifi_gpio23" SITE "R5"; # JTAG TDI
+# LOCATE COMP "wifi_gpio19" SITE "V4"; # JTAG TDO
+IOBUF  PORT "wifi_en" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "wifi_rxd" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "wifi_txd" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "wifi_gpio0" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "wifi_gpio5" PULLMODE=DOWN IO_TYPE=LVCMOS33 DRIVE=4; # pull down or drive 0 for esp32 programming
+IOBUF  PORT "wifi_gpio16" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "wifi_gpio17" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+# IOBUF  PORT "prog_done" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+
+## PCB antenna 433 MHz (may be also used for FM) "usb" sheet
+LOCATE COMP "ant_433mhz" SITE "G1";
+IOBUF  PORT "ant_433mhz" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+
+## Second USB port "US2" going directly into FPGA "usb", "ram" sheet
+LOCATE COMP "usb_fpga_dp" SITE "E16"; # single ended or differential input only
+LOCATE COMP "usb_fpga_dn" SITE "F16";
+IOBUF  PORT "usb_fpga_dp" PULLMODE=NONE IO_TYPE=LVCMOS33D DRIVE=16;
+IOBUF  PORT "usb_fpga_dn" PULLMODE=NONE IO_TYPE=LVCMOS33D DRIVE=16;
+LOCATE COMP "usb_fpga_bd_dp" SITE "D15"; # single-ended bidirectional
+LOCATE COMP "usb_fpga_bd_dn" SITE "E15";
+IOBUF  PORT "usb_fpga_bd_dp" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF  PORT "usb_fpga_bd_dn" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+LOCATE COMP "usb_fpga_pu_dp" SITE "B12"; # pull up/down control
+LOCATE COMP "usb_fpga_pu_dn" SITE "C12";
+IOBUF  PORT "usb_fpga_pu_dp" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
+IOBUF  PORT "usb_fpga_pu_dn" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
+
+## JTAG ESP-32 "usb" sheet
+# connected to FT231X and ESP-32
+# commented out because those are dedicated pins, not directly useable as GPIO
+# but could be used by some vendor-specific JTAG bridging (boundary scan) module
+#LOCATE COMP "jtag_tdi" SITE "R5"; # FTDI_nRI   FPGA receives
+#LOCATE COMP "jtag_tdo" SITE "V4"; # FTDI_nCTS  FPGA transmits
+#LOCATE COMP "jtag_tck" SITE "T5"; # FTDI_nDSR  FPGA receives
+#LOCATE COMP "jtag_tms" SITE "U5"; # FTDI_nDCD  FPGA receives
+#IOBUF  PORT "jtag_tdi" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+#IOBUF  PORT "jtag_tdo" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+#IOBUF  PORT "jtag_tck" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+#IOBUF  PORT "jtag_tms" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+
+## SDRAM "ram" sheet
+LOCATE COMP "sdram_clk" SITE "F19";
+LOCATE COMP "sdram_cke" SITE "F20";
+LOCATE COMP "sdram_csn" SITE "P20";
+LOCATE COMP "sdram_wen" SITE "T20";
+LOCATE COMP "sdram_rasn" SITE "R20";
+LOCATE COMP "sdram_casn" SITE "T19";
+LOCATE COMP "sdram_a[0]" SITE "M20";
+LOCATE COMP "sdram_a[1]" SITE "M19";
+LOCATE COMP "sdram_a[2]" SITE "L20";
+LOCATE COMP "sdram_a[3]" SITE "L19";
+LOCATE COMP "sdram_a[4]" SITE "K20";
+LOCATE COMP "sdram_a[5]" SITE "K19";
+LOCATE COMP "sdram_a[6]" SITE "K18";
+LOCATE COMP "sdram_a[7]" SITE "J20";
+LOCATE COMP "sdram_a[8]" SITE "J19";
+LOCATE COMP "sdram_a[9]" SITE "H20";
+LOCATE COMP "sdram_a[10]" SITE "N19";
+LOCATE COMP "sdram_a[11]" SITE "G20";
+LOCATE COMP "sdram_a[12]" SITE "G19";
+LOCATE COMP "sdram_ba[0]" SITE "P19";
+LOCATE COMP "sdram_ba[1]" SITE "N20";
+LOCATE COMP "sdram_dqm[0]" SITE "U19";
+LOCATE COMP "sdram_dqm[1]" SITE "E20";
+LOCATE COMP "sdram_d[0]" SITE "J16";
+LOCATE COMP "sdram_d[1]" SITE "L18";
+LOCATE COMP "sdram_d[2]" SITE "M18";
+LOCATE COMP "sdram_d[3]" SITE "N18";
+LOCATE COMP "sdram_d[4]" SITE "P18";
+LOCATE COMP "sdram_d[5]" SITE "T18";
+LOCATE COMP "sdram_d[6]" SITE "T17";
+LOCATE COMP "sdram_d[7]" SITE "U20";
+LOCATE COMP "sdram_d[8]" SITE "E19";
+LOCATE COMP "sdram_d[9]" SITE "D20";
+LOCATE COMP "sdram_d[10]" SITE "D19";
+LOCATE COMP "sdram_d[11]" SITE "C20";
+LOCATE COMP "sdram_d[12]" SITE "E18";
+LOCATE COMP "sdram_d[13]" SITE "F18";
+LOCATE COMP "sdram_d[14]" SITE "J18";
+LOCATE COMP "sdram_d[15]" SITE "J17";
+IOBUF PORT "sdram_clk" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_cke" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_csn" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_wen" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_rasn" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_casn" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_a[0]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_a[1]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_a[2]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_a[3]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_a[4]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_a[5]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_a[6]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_a[7]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_a[8]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_a[9]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_a[10]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_a[11]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_a[12]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_ba[0]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_ba[1]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_dqm[0]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_dqm[1]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_d[0]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_d[1]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_d[2]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_d[3]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_d[4]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_d[5]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_d[6]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_d[7]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_d[8]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_d[9]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_d[10]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_d[11]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_d[12]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_d[13]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_d[14]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "sdram_d[15]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
+
+# GPDI differential interface (Video) "gpdi" sheet
+LOCATE COMP "gpdi_dp[0]" SITE "A16"; # Blue +
+LOCATE COMP "gpdi_dn[0]" SITE "B16"; # Blue -
+LOCATE COMP "gpdi_dp[1]" SITE "A14"; # Green +
+LOCATE COMP "gpdi_dn[1]" SITE "C14"; # Green -
+LOCATE COMP "gpdi_dp[2]" SITE "A12"; # Red +
+LOCATE COMP "gpdi_dn[2]" SITE "A13"; # Red -
+LOCATE COMP "gpdi_dp[3]" SITE "A17"; # Clock +
+LOCATE COMP "gpdi_dn[3]" SITE "B18"; # Clock -
+LOCATE COMP "gpdi_util" SITE "A19"; # add 10k parallel to C
+LOCATE COMP "gpdi_hpd" SITE "B20"; # add 549ohm parallel to C
+LOCATE COMP "gpdi_cec" SITE "A18";
+LOCATE COMP "gpdi_sda" SITE "B19"; # I2C shared with RTC
+LOCATE COMP "gpdi_scl" SITE "E12"; # I2C shared with RTC C12->E12
+IOBUF PORT "gpdi_dp[0]" IO_TYPE=LVCMOS33D DRIVE=4;
+IOBUF PORT "gpdi_dn[0]" IO_TYPE=LVCMOS33D DRIVE=4;
+IOBUF PORT "gpdi_dp[1]" IO_TYPE=LVCMOS33D DRIVE=4;
+IOBUF PORT "gpdi_dn[1]" IO_TYPE=LVCMOS33D DRIVE=4;
+IOBUF PORT "gpdi_dp[2]" IO_TYPE=LVCMOS33D DRIVE=4;
+IOBUF PORT "gpdi_dn[2]" IO_TYPE=LVCMOS33D DRIVE=4;
+IOBUF PORT "gpdi_dp[3]" IO_TYPE=LVCMOS33D DRIVE=4;
+IOBUF PORT "gpdi_dn[3]" IO_TYPE=LVCMOS33D DRIVE=4;
+IOBUF PORT "gpdi_util" IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "gpdi_hpd" IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "gpdi_cec" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "gpdi_sda" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT "gpdi_scl" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+
+# GPIO (default single-ended) "gpio", "ram", "gpdi" sheet
+# Pins enumerated gp[0-27], gn[0-27].
+# With differential mode enabled on Lattice,
+# gp[] (+) are used, gn[] (-) are ignored from design
+# as they handle inverted signal by default.
+# To enable differential, rename LVCMOS33->LVCMOS33D
+# FEMALE ANGLED (90 deg PMOD) on TOP or
+# MALE VERTICAL ( 0 deg pins) on BOTTOM and flat cable
+LOCATE COMP "gp[0]"  SITE "B11"; # PCLK
+LOCATE COMP "gn[0]"  SITE "C11"; # PCLK
+LOCATE COMP "gp[1]"  SITE "A10"; # PCLK
+LOCATE COMP "gn[1]"  SITE "A11"; # PCLK
+LOCATE COMP "gp[2]"  SITE "A9";  # GR_PCLK
+LOCATE COMP "gn[2]"  SITE "B10"; # GR_PCLK
+LOCATE COMP "gp[3]"  SITE "B9";
+LOCATE COMP "gn[3]"  SITE "C10";
+LOCATE COMP "gp[4]"  SITE "A7";
+LOCATE COMP "gn[4]"  SITE "A8";
+LOCATE COMP "gp[5]"  SITE "C8";
+LOCATE COMP "gn[5]"  SITE "B8";
+LOCATE COMP "gp[6]"  SITE "C6";
+LOCATE COMP "gn[6]"  SITE "C7";
+IOBUF PORT  "gp[0]"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[0]"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[1]"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[1]"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[2]"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[2]"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[3]"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[3]"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[4]"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[4]"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[5]"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[5]"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[6]"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[6]"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+LOCATE COMP "gp[7]"  SITE "A6";
+LOCATE COMP "gn[7]"  SITE "B6";
+LOCATE COMP "gp[8]"  SITE "A4"; # DIFF
+LOCATE COMP "gn[8]"  SITE "A5"; # DIFF
+LOCATE COMP "gp[9]"  SITE "A2"; # DIFF
+LOCATE COMP "gn[9]"  SITE "B1"; # DIFF
+LOCATE COMP "gp[10]" SITE "C4"; # DIFF
+LOCATE COMP "gn[10]" SITE "B4"; # DIFF
+LOCATE COMP "gp[11]" SITE "F4"; # DIFF wifi_gpio26
+LOCATE COMP "gn[11]" SITE "E3"; # DIFF wifi_gpio25
+LOCATE COMP "gp[12]" SITE "G3"; # DIFF wifi_gpio33 PCLK
+LOCATE COMP "gn[12]" SITE "F3"; # DIFF wifi_gpio32 PCLK
+LOCATE COMP "gp[13]" SITE "H4"; # DIFF wifi_gpio35
+LOCATE COMP "gn[13]" SITE "G5"; # DIFF wifi_gpio34
+IOBUF PORT  "gp[7]"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[7]"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[8]"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[8]"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[9]"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[9]"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[10]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[10]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[11]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[11]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[12]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[12]" PULLMODE=NONE IO_TYPE=LVCMOS33;
+FREQUENCY PORT "gn[12]" 50 MHZ;
+IOBUF PORT  "gp[13]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[13]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+LOCATE COMP "gp[14]" SITE "U18"; # DIFF ADC AIN1
+LOCATE COMP "gn[14]" SITE "U17"; # DIFF ADC AIN0
+LOCATE COMP "gp[15]" SITE "N17"; # DIFF ADC AIN3
+LOCATE COMP "gn[15]" SITE "P16"; # DIFF ADC AIN2
+LOCATE COMP "gp[16]" SITE "N16"; # DIFF ADC AIN5
+LOCATE COMP "gn[16]" SITE "M17"; # DIFF ADC AIN4
+LOCATE COMP "gp[17]" SITE "L16"; # DIFF ADC AIN7 GR_PCLK
+LOCATE COMP "gn[17]" SITE "L17"; # DIFF ADC AIN6
+LOCATE COMP "gp[18]" SITE "H18"; # DIFF
+LOCATE COMP "gn[18]" SITE "H17"; # DIFF
+LOCATE COMP "gp[19]" SITE "F17"; # DIFF
+LOCATE COMP "gn[19]" SITE "G18"; # DIFF
+LOCATE COMP "gp[20]" SITE "D18"; # DIFF
+LOCATE COMP "gn[20]" SITE "E17"; # DIFF
+IOBUF PORT  "gp[14]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[14]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[15]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[15]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[16]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[16]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[17]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[17]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[18]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[18]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[19]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[19]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[20]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[20]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+LOCATE COMP "gp[21]" SITE "C18"; # DIFF
+LOCATE COMP "gn[21]" SITE "D17"; # DIFF
+LOCATE COMP "gp[22]" SITE "B15";
+LOCATE COMP "gn[22]" SITE "C15";
+LOCATE COMP "gp[23]" SITE "B17";
+LOCATE COMP "gn[23]" SITE "C17";
+LOCATE COMP "gp[24]" SITE "C16";
+LOCATE COMP "gn[24]" SITE "D16";
+LOCATE COMP "gp[25]" SITE "D14";
+LOCATE COMP "gn[25]" SITE "E14";
+LOCATE COMP "gp[26]" SITE "B13";
+LOCATE COMP "gn[26]" SITE "C13";
+LOCATE COMP "gp[27]" SITE "D13";
+LOCATE COMP "gn[27]" SITE "E13";
+IOBUF PORT  "gp[21]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[21]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[22]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[22]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[23]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[23]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[24]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[24]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[25]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[25]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[26]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[26]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp[27]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn[27]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+
+## GPIO repeated as individual signals (non-vector)
+# Allows mixed input, output, bidirectional, clock, differential
+# If any of individual gp is used, then don't use gp[] vector.
+# Same for gn and gn[].
+# FEMALE ANGLED (90 deg PMOD) on TOP or
+# MALE VERTICAL ( 0 deg pins) on BOTTOM and flat cable
+LOCATE COMP "gp0"  SITE "B11"; # PCLK
+LOCATE COMP "gn0"  SITE "C11"; # PCLK
+LOCATE COMP "gp1"  SITE "A10"; # PCLK
+LOCATE COMP "gn1"  SITE "A11"; # PCLK
+LOCATE COMP "gp2"  SITE "A9";  # GR_PCLK
+LOCATE COMP "gn2"  SITE "B10"; # GR_PCLK
+LOCATE COMP "gp3"  SITE "B9";
+LOCATE COMP "gn3"  SITE "C10";
+LOCATE COMP "gp4"  SITE "A7";
+LOCATE COMP "gn4"  SITE "A8";
+LOCATE COMP "gp5"  SITE "C8";
+LOCATE COMP "gn5"  SITE "B8";
+LOCATE COMP "gp6"  SITE "C6";
+LOCATE COMP "gn6"  SITE "C7";
+IOBUF PORT  "gp0"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn0"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp1"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn1"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp2"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn2"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp3"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn3"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp4"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn4"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp5"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn5"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp6"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn6"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+LOCATE COMP "gp7"  SITE "A6";
+LOCATE COMP "gn7"  SITE "B6";
+LOCATE COMP "gp8"  SITE "A4"; # DIFF
+LOCATE COMP "gn8"  SITE "A5"; # DIFF
+LOCATE COMP "gp9"  SITE "A2"; # DIFF
+LOCATE COMP "gn9"  SITE "B1"; # DIFF
+LOCATE COMP "gp10" SITE "C4"; # DIFF
+LOCATE COMP "gn10" SITE "B4"; # DIFF
+LOCATE COMP "gp11" SITE "F4"; # DIFF wifi_gpio26
+LOCATE COMP "gn11" SITE "E3"; # DIFF wifi_gpio25
+LOCATE COMP "gp12" SITE "G3"; # DIFF wifi_gpio33 PCLK
+LOCATE COMP "gn12" SITE "F3"; # DIFF wifi_gpio32 PCLK
+LOCATE COMP "gp13" SITE "H4"; # DIFF wifi_gpio35
+LOCATE COMP "gn13" SITE "G5"; # DIFF wifi_gpio34
+# wifi sharing PCB v2.0.6-v3.0.8
+# prior to v2.0.6 see schematics
+IOBUF PORT  "gp7"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn7"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp8"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn8"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp9"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn9"  PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp10" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn10" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp11" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn11" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp12" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn12" PULLMODE=NONE IO_TYPE=LVCMOS33;
+FREQUENCY PORT "gn12" 50 MHZ;
+IOBUF PORT  "gp13" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn13" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+LOCATE COMP "gp14" SITE "U18"; # DIFF ADC AIN1
+LOCATE COMP "gn14" SITE "U17"; # DIFF ADC AIN0
+LOCATE COMP "gp15" SITE "N17"; # DIFF ADC AIN3
+LOCATE COMP "gn15" SITE "P16"; # DIFF ADC AIN2
+LOCATE COMP "gp16" SITE "N16"; # DIFF ADC AIN5
+LOCATE COMP "gn16" SITE "M17"; # DIFF ADC AIN4
+LOCATE COMP "gp17" SITE "L16"; # DIFF ADC AIN7 GR_PCLK
+LOCATE COMP "gn17" SITE "L17"; # DIFF ADC AIN6
+LOCATE COMP "gp18" SITE "H18"; # DIFF
+LOCATE COMP "gn18" SITE "H17"; # DIFF
+LOCATE COMP "gp19" SITE "F17"; # DIFF
+LOCATE COMP "gn19" SITE "G18"; # DIFF
+LOCATE COMP "gp20" SITE "D18"; # DIFF
+LOCATE COMP "gn20" SITE "E17"; # DIFF
+IOBUF PORT  "gp14" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn14" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp15" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn15" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp16" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn16" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp17" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn17" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp18" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn18" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp19" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn19" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp20" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn20" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+LOCATE COMP "gp21" SITE "C18"; # DIFF
+LOCATE COMP "gn21" SITE "D17"; # DIFF
+LOCATE COMP "gp22" SITE "B15";
+LOCATE COMP "gn22" SITE "C15";
+LOCATE COMP "gp23" SITE "B17";
+LOCATE COMP "gn23" SITE "C17";
+LOCATE COMP "gp24" SITE "C16";
+LOCATE COMP "gn24" SITE "D16";
+LOCATE COMP "gp25" SITE "D14";
+LOCATE COMP "gn25" SITE "E14";
+LOCATE COMP "gp26" SITE "B13";
+LOCATE COMP "gn26" SITE "C13";
+LOCATE COMP "gp27" SITE "D13";
+LOCATE COMP "gn27" SITE "E13";
+IOBUF PORT  "gp21" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn21" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp22" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn22" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp23" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn23" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp24" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn24" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp25" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn25" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp26" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn26" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gp27" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+IOBUF PORT  "gn27" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+
+## PROGRAMN (reload bitstream from FLASH, exit from bootloader)
+# PCB v2.0.5 and higher
+LOCATE COMP "user_programn" SITE "M4";
+IOBUF  PORT "user_programn" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
+
+## SHUTDOWN "power", "ram" sheet (connected from PCB v1.7.5)
+# on PCB v1.7 shutdown is not connected to FPGA
+LOCATE COMP "shutdown" SITE "G16"; # FPGA receives
+IOBUF  PORT "shutdown" PULLMODE=DOWN IO_TYPE=LVCMOS33 DRIVE=4;
diff --git a/src/xeda/dataclass.py b/src/xeda/dataclass.py
new file mode 100644
index 00000000..7b67b564
--- /dev/null
+++ b/src/xeda/dataclass.py
@@ -0,0 +1,128 @@
+"""Interchangable dataclass abstraction"""
+from __future__ import annotations
+
+import logging
+from abc import ABCMeta
+from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TypeVar
+
+import attrs
+
+# pylint: disable=no-name-in-module
+from pydantic import (
+    BaseModel,
+    BaseConfig,
+    Extra,
+    Field,
+    ValidationError,
+    root_validator,
+    validator,
+)
+
+__all__ = [
+    "XedaBaseModel",
+    "XedaBaseModeAllowExtra",
+    "xeda_model",  # decorator
+    "Field",
+    "validator",
+    "root_validator",
+    "Extra",
+    "asdict",
+    "define",
+    "ValidationError",
+    "validation_errors",
+]
+
+
+log = logging.getLogger(__name__)
+
+
+# WIP: interchangable dataclass backend
+def define(maybe_cls: Optional[Type[Any]], **kwargs: Any) -> Any:
+    return attrs.define(maybe_cls, **kwargs)
+
+
+def field(
+    default: Any = attrs.NOTHING,
+    *,
+    description: Optional[str] = None,
+    validator_: Optional[Callable[..., None]] = None,
+    converter: Optional[Callable[..., Any]] = None,
+    factory: Optional[Callable[[], Any]] = None,
+    on_setattr: Any = None,
+    **kwargs: Any,
+) -> Any:
+    metadata = None
+    if description is not None:
+        metadata = {"description": description}
+    return attrs.field(
+        default=default,
+        validator=validator_,
+        converter=converter,
+        factory=factory,
+        on_setattr=on_setattr,
+        metadata=metadata,
+        **kwargs,
+    )
+
+
+def asdict(inst: Any, filter_: Optional[Callable[..., bool]] = None) -> Dict[str, Any]:
+    if isinstance(inst, BaseModel):
+        assert filter_ is None
+        return inst.dict()
+    return attrs.asdict(inst, filter=filter_)
+
+
+class XedaBaseModel(BaseModel):
+    class Config(BaseConfig):
+        validate_assignment = True
+        extra = Extra.forbid
+        arbitrary_types_allowed = True
+        use_enum_values = True
+
+    # def validate(self):
+    #     *_, validation_error = validate_model(self.__class__, self.__dict__)
+    #     if validation_error:
+    #         raise validation_error
+
+
+class XedaBaseModeAllowExtra(XedaBaseModel, metaclass=ABCMeta):
+    class Config(XedaBaseModel.Config):
+        extra = Extra.allow
+
+
+def error_type_and_context(error: Dict[str, Any]) -> str:
+    ctx = error.get("ctx", {})
+    return str(error.get("type")) + "".join(f"; {k}={v}" for k, v in ctx.items())
+
+
+def validation_errors(errors: List[Dict[str, Any]]) -> List[Tuple[str, str, str]]:
+    return [
+        (
+            " -> ".join(str(loc) for loc in e.get("loc", [])),
+            e.get("msg", "???"),
+            error_type_and_context(e),
+        )
+        for e in errors
+    ]
+
+
+T = TypeVar("T", bound=object)
+
+
+def xeda_model(
+    maybe_class: Optional[Type[T]] = None, /, *, allow_extra: bool = False
+) -> Type[T]:
+    """decorator replacement for dataclasses"""
+    # This is a WIP
+    # FIXME: does not work with classes with inheritance
+
+    def wrap(clz: Type[T]) -> Type[T]:
+        base_model: Type[BaseModel] = (
+            XedaBaseModeAllowExtra if allow_extra else XedaBaseModel
+        )
+        bases = (clz,) if issubclass(clz, base_model) else (clz, base_model)
+        eclz = type(clz.__name__, bases, dict(clz.__dict__))
+
+        return eclz
+
+    return wrap if maybe_class is None else wrap(maybe_class)  # type: ignore
diff --git a/src/xeda/design.py b/src/xeda/design.py
new file mode 100644
index 00000000..0b401821
--- /dev/null
+++ b/src/xeda/design.py
@@ -0,0 +1,363 @@
+import hashlib
+import logging
+import os
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, TypeVar, Union
+
+from .dataclass import (
+    Extra,
+    Field,
+    ValidationError,
+    XedaBaseModel,
+    root_validator,
+    validation_errors,
+    validator,
+)
+from .utils import WorkingDirectory, toml_load
+
+log = logging.getLogger(__name__)
+
+__all__ = [
+    "Design",
+    "DesignSource",
+    "FileResource",
+    "VhdlSettings",
+    "LanguageSettings",
+    "Clock",
+]
+
+
+class DesignValidationError(Exception):
+    def __init__(
+        self,
+        errors: List[Tuple[str, str, str]],
+        data: Dict[str, Any],
+        *args: object,
+        design_root: Union[None, str, os.PathLike] = None,
+        design_name: Optional[str] = None,
+        file: Optional[str] = None,
+    ) -> None:
+        super().__init__(*args)
+        self.errors = errors  # location, msg, type/context
+        self.data = data
+        self.design_root = design_root
+        self.design_name = design_name
+        self.file = file
+
+    def __str__(self) -> str:
+        name = self.design_name or self.data.get("name")
+        return "{}: {} error{} validating design{}:\n{}".format(
+            self.__class__.__qualname__,
+            len(self.errors),
+            "s" if len(self.errors) > 1 else "",
+            f" {name}" if name else "",
+            "\n".join(
+                "{}{} ({})\n".format(f"{loc}:\n   " if loc else "", msg, ctx)
+                for loc, msg, ctx in self.errors
+            ),
+        )
+
+
+class FileResource:
+    def __init__(
+        self,
+        path: Union[str, os.PathLike, Dict[str, str]],
+        _root_path: Optional[Path] = None,
+        **data: Any,
+    ) -> None:
+        self._content_hash: Optional[str] = None
+        try:
+            if isinstance(path, dict):
+                if "file" not in path:
+                    raise ValueError("Required field 'file' is missing.")
+                path = path["file"]
+            p = Path(path)
+            if not p.is_absolute():
+                if not _root_path:
+                    _root_path = Path.cwd()
+                p = _root_path / p
+            self.file = p.resolve(strict=True)
+        except FileNotFoundError as e:
+            log.critical("Design resource '%s' does not exist!", path)
+            raise e from None
+
+    @property
+    def hash(self) -> str:
+        """return hash of file content"""
+        if self._content_hash is None:
+            with open(self.file, "rb") as f:
+                self._content_hash = hashlib.sha256(f.read()).hexdigest()
+        return self._content_hash
+
+    def __eq__(self, other: Any) -> bool:
+        if not isinstance(other, FileResource):
+            return False
+        return self.hash == other.hash and self.file.samefile(other.file)
+
+    def __hash__(self) -> int:
+        # path is already absolute
+        return hash((self.hash, str(self.file)))
+
+    def __str__(self) -> str:
+        return str(self.file)
+
+    def __repr__(self) -> str:
+        return "FileResource:" + self.__str__()
+
+
+class DesignSource(FileResource):
+    def __init__(
+        self,
+        path: Union[str, os.PathLike, Dict[str, str]],
+        typ: Optional[str] = None,
+        standard: Optional[str] = None,
+        variant: Optional[str] = None,
+        _root_path: Optional[Path] = None,
+        **data: Any,
+    ) -> None:
+        super().__init__(path, _root_path, **data)
+
+        def type_from_suffix(path: Path) -> Tuple[Optional[str], Optional[str]]:
+            type_variants_map = {
+                ("vhdl", variant): ["vhd", "vhdl"],
+                ("verilog", variant): ["v"],
+                ("systemverilog", variant): ["sv"],
+                ("bsv", variant): ["bsv"],
+                ("bs", variant): ["bs"],
+            }
+            for h, suffixes in type_variants_map.items():
+                if path.suffix[1:] in suffixes:
+                    return h
+            return None, None
+
+        self.type, self.variant = (typ, variant) if typ else type_from_suffix(self.file)
+        if standard and len(standard) == 4:
+            if standard.startswith("20") or standard.startswith("19"):
+                standard = standard[2:]
+        self.standard = standard
+
+    def __eq__(self, other: Any) -> bool:  # pylint: disable=useless-super-delegation
+        # added attributes do not change semantic equality
+        return super().__eq__(other)
+
+    def __hash__(self) -> int:  # pylint: disable=useless-super-delegation
+        # added attributes do not change semantic identity
+        return super().__hash__()
+
+
+DefineType = Any
+# the order matters!
+# Union[FileResource, int, bool, float, str]
+
+# Tuple -> Tuple[()] (empty tuple), but pydantic 1.9.0 + Python 3.9 typing do not like it
+# Tuple of 0, 1, or 2 strings:
+Tuple012 = Union[Tuple[str, ...], Tuple[str], Tuple[str, str]]  # xtype: ignore
+
+
+class DVSettings(XedaBaseModel):  # type: ignore
+    """Design/Verification settings"""
+
+    sources: List[DesignSource]
+    generics: Dict[str, DefineType] = Field(
+        default={},
+        description="Top-level generics/defines specified as a mapping",
+        alias="parameters",
+        allow_population_by_field_name=True,
+        has_alias=True,
+    )  # top defines/generics
+    parameters: Dict[str, DefineType] = Field(
+        default={},
+        description="Top-level generics/defines specified as a mapping",
+        alias="generics",
+        allow_population_by_field_name=True,
+        has_alias=True,
+    )
+    defines: Dict[str, DefineType] = Field(default={})
+
+    # pylint: disable=no-self-argument
+    @root_validator()
+    def the_root_validator(cls, values: Dict[str, Any]) -> Dict[str, Any]:
+        value = values.get("parameters")
+        if not value:
+            value = values.get("generics")
+        if value:
+            for k, v in value.items():
+                if isinstance(v, dict) and "file" in v:
+                    value[k] = FileResource(v).__str__()
+            values["generics"] = value
+            values["parameters"] = value
+        return values
+
+    @validator("sources", pre=True, always=False)
+    def sources_to_files(
+        cls, sources: List[Union[DesignSource, str, os.PathLike, Path, Dict[str, Any]]]
+    ) -> List[DesignSource]:
+        ds = []
+        for src in sources:
+            if not isinstance(src, DesignSource):
+                try:
+                    src = DesignSource(src)
+                except FileNotFoundError as e:
+                    raise ValueError(
+                        f"Source file: {src} was not found: {e.strerror} {e.filename}"
+                    ) from None
+            ds.append(src)
+        return ds
+
+
+class Clock(XedaBaseModel):
+    port: Optional[str]
+
+
+class RtlSettings(DVSettings):
+    top: str = Field(
+        description="Toplevel RTL module/entity",
+    )
+    clock: Clock = Clock(port=None)  # TODO rename to primary_clock?
+    clocks: Dict[str, Clock] = {}
+    clock_port: Optional[str] = None  # TODO remove?
+
+    @root_validator(pre=False)
+    def rtl_settings_validate(cls, values):
+        clock = values.get("clock")
+        clock_port = values.get("clock_port")
+        if not clock.port:
+            clock.port = clock_port
+        if not clock_port:
+            values["clock_port"] = clock.port
+        if not values.get("clocks"):
+            values["clocks"] = {"main_clock": clock}
+        return values
+
+
+class TbSettings(DVSettings):
+    top: Tuple012 = Field(
+        tuple(),
+        description="Toplevel testbench module(s), specified as a tuple of strings. In addition to the primary toplevel, a secondary toplevel module can also be specified.",
+    )
+    sources: List[DesignSource] = []
+    uut: Optional[str] = Field(
+        None, description="instance name of the unit under test in the testbench"
+    )
+    cocotb: bool = Field(False, description="testbench is based on cocotb framework")
+
+    @validator("top", pre=True)
+    def top_validator(cls, top: Union[None, str, Sequence[str], Tuple012]) -> Tuple012:
+        if top:
+            if isinstance(top, str):
+                return (top,)
+            if isinstance(top, (tuple, list, Sequence)):
+                assert len(top) <= 2
+                return tuple(top)
+        return tuple()
+
+
+class LanguageSettings(XedaBaseModel):
+    standard: Optional[str] = Field(
+        None,
+        description="Standard version",
+        alias="version",
+        has_alias=True,
+        allow_population_by_field_name=True,
+    )
+    version: Optional[str] = Field(
+        None,
+        description="Standard version",
+        alias="standard",
+        has_alias=True,
+        allow_population_by_field_name=True,
+    )
+
+    @validator("standard", pre=True)
+    def two_digit_standard(cls, standard, values):
+        if isinstance(standard, int):
+            standard = str(standard)
+        elif not isinstance(standard, str):
+            raise ValueError("standard should be of type string")
+        if standard and len(standard) == 4:
+            if standard.startswith("20") or standard.startswith("19"):
+                standard = standard[2:]
+        return standard
+
+    @root_validator(pre=True)
+    def language_root_validator(cls, values):
+        if "standard" in values:
+            values["version"] = values["standard"]
+        return values
+
+
+class VhdlSettings(LanguageSettings):
+    synopsys: bool = False
+
+
+class Language(XedaBaseModel):
+    vhdl: VhdlSettings = VhdlSettings()  # type: ignore
+    verilog: LanguageSettings = LanguageSettings()  # type: ignore
+
+
+class Design(XedaBaseModel):
+    name: str
+    rtl: RtlSettings
+    tb: TbSettings = TbSettings()  # type: ignore
+    language: Language = Language()
+
+    class Config(XedaBaseModel.Config):
+        extra = Extra.allow
+
+    def __init__(
+        self,
+        design_root: Union[None, str, os.PathLike] = None,
+        **data: Any,
+    ) -> None:
+        with WorkingDirectory(design_root):
+            try:
+                super().__init__(**data)
+            except ValidationError as e:
+                raise DesignValidationError(
+                    validation_errors(e.errors()), data=data, design_root=design_root  # type: ignore
+                ) from None
+
+    @property
+    def sim_sources(self) -> List[DesignSource]:
+        if not self.tb:
+            return []
+        return self.rtl.sources + [
+            src
+            for src in self.tb.sources
+            if src not in self.rtl.sources and src.type in ("vhdl", "verilog")
+        ]
+
+    @property
+    def sim_tops(self) -> Tuple012:
+        if self.tb:
+            if self.tb.cocotb and self.rtl.top:
+                return (self.rtl.top,)
+            if self.tb.top is not None:
+                return self.tb.top
+        return tuple()
+
+    T = TypeVar("T", bound="Design")
+
+    @classmethod
+    def from_toml(
+        cls: Type[T],
+        design_file: Union[str, os.PathLike],
+        design_root: Union[None, str, os.PathLike] = None,
+    ) -> T:
+        """Load and validate a design description from TOML file"""
+        if not isinstance(design_file, Path):
+            design_file = Path(design_file)
+        design_dict = toml_load(design_file)
+        # Default value for design_root is the folder containing the design description file.
+        if design_root is None:
+            design_root = design_file.parent
+        try:
+            return cls(design_root=design_root, **design_dict)
+        except DesignValidationError as e:
+            raise DesignValidationError(  # add design_file to the emitted exception
+                e.errors,
+                data=e.data,
+                design_root=e.design_root,
+                design_name=e.design_name,
+                file=str(design_file),
+            ) from None
diff --git a/src/xeda/flow_runner/__init__.py b/src/xeda/flow_runner/__init__.py
new file mode 100644
index 00000000..57095bfa
--- /dev/null
+++ b/src/xeda/flow_runner/__init__.py
@@ -0,0 +1,376 @@
+"""Flow runner"""
+import hashlib
+import importlib
+import json
+import logging
+import os
+import time
+from datetime import datetime, timedelta
+from pathlib import Path, PosixPath
+from typing import Any, Dict, Mapping, Optional, Set, Type, Union
+
+# fmt: off
+from pathvalidate import sanitize_filename  # pyright: reportPrivateImportUsage=none
+
+from rich import box, print_json
+from rich.style import Style
+from rich.table import Table
+from rich.text import Text
+
+from ..console import console
+from ..dataclass import asdict
+from ..design import Design
+from ..flows.flow import Flow, FlowDependencyFailure, registered_flows
+from ..tool import NonZeroExitCode
+from ..utils import WorkingDirectory, backup_existing, dump_json, snakecase_to_camelcase
+from ..version import __version__
+
+__all__ = [
+    "get_flow_class",
+    "FlowNotFoundError",
+    "FlowRunner",
+    "DefaultRunner",
+    "print_results",
+]
+
+log = logging.getLogger(__name__)
+
+
+def print_results(
+    flow: Flow,
+    results: Optional[Dict[str, Any]] = None,
+    title: Optional[str] = None,
+    subset: Optional[Set[str]] = None,
+    skip_if_empty: Optional[Set[str]] = None,
+) -> None:
+    if results is None:
+        results = flow.results
+    console.print()
+    table = Table(
+        title=title,
+        title_style=Style(frame=True, bold=True),
+        show_header=False,
+        box=box.ROUNDED,
+        show_lines=True,
+    )
+    table.add_column(style="bold", no_wrap=True)
+    table.add_column(justify="right")
+    for k, v in results.items():
+        if skip_if_empty and k in skip_if_empty and not v:
+            continue
+        if v is not None and not k.startswith("_"):
+            if k == "success":
+                text = "OK :heavy_check_mark-text:" if v else "FAILED :cross_mark-text:"
+                color = "green" if v else "red"
+                table.add_row("Status", text, style=Style(color=color))
+                continue
+            if subset and k not in subset:
+                continue
+            if k == "design":
+                table.add_row("Design Name", Text(v), style=Style(dim=True))
+                continue
+            if k == "flow":
+                table.add_row("Flow Name", Text(v), style=Style(dim=True))
+                continue
+            if k == "runtime":
+                table.add_row(
+                    "Running Time",
+                    str(timedelta(seconds=round(v))),
+                    style=Style(dim=True),
+                )
+                continue
+            if isinstance(v, float):
+                v = f"{v:.3f}"
+            table.add_row(k, str(v))
+    console.print(table)
+
+
+class FlowNotFoundError(Exception):
+    pass
+
+
+def get_flow_class(
+    flow_name: str, module_name: str = "xeda.flows", package: str = __package__
+) -> Type[Flow]:
+    _mod, flow_class = registered_flows.get(flow_name, (None, None))
+    if flow_class is None:
+        log.warning(
+            "Flow %s was not found in registered flows. Trying to load using importlib.import_module",
+            flow_name,
+        )
+        try:
+            module = importlib.import_module(module_name)
+        except ModuleNotFoundError as e:
+            raise FlowNotFoundError() from e
+        flow_class_name = snakecase_to_camelcase(flow_name)
+        if module:
+            try:
+                flow_class = getattr(module, flow_class_name)
+            except AttributeError:
+                pass
+        if not flow_class or not issubclass(flow_class, Flow):
+            raise FlowNotFoundError()
+    return flow_class
+
+
+def _semantic_hash(data: Any, debug=False) -> str:
+    def _sorted_dict_str(data: Any) -> Any:
+        if isinstance(data, (dict, Mapping)):
+            return {k: _sorted_dict_str(data[k]) for k in sorted(data.keys())}
+        if isinstance(data, list):
+            return [_sorted_dict_str(val) for val in data]
+        if hasattr(data, "__dict__"):
+            return _sorted_dict_str(data.__dict__)
+        return str(data)
+
+    def _get_digest(b: bytes) -> str:
+        return hashlib.sha256(b).hexdigest()[:16]
+
+    r = repr(_sorted_dict_str(data))
+    if debug:
+        print("\nSEMANTIC_HASH:", r)
+    return _get_digest(bytes(r, "UTF-8"))
+
+
+class FlowRunner:
+    """
+    Manage running flows and their dependencies.
+    1. Instantiate instance of flow class with proper settings assigned (__init__)
+    2. call Flow.init()
+    3. Run all dependency flows (asked by the flow, during Flow.init)
+    3. Run the flow by calling run()
+    4. Run flow's parse_reports() ## TODO parse_reports will be renamed
+    5. Evaluate and print the results
+    """
+
+    def __init__(
+        self,
+        xeda_run_dir: Union[str, os.PathLike] = "xeda_run",
+        debug: bool = False,
+        dump_settings_json: bool = True,
+        display_results: bool = True,
+        dump_results_json: bool = True,
+        cached_dependencies: bool = False,  # do not run dependencies if previous run results exist. Uses flow run_dir names including design and flow.settings hashes
+        run_in_existing_dir: bool = False,  # DO NOT USE! Only for development!
+    ) -> None:
+        if debug:
+            log.setLevel(logging.DEBUG)
+            log.root.setLevel(logging.DEBUG)
+        self.debug = debug
+        log.debug("%s xeda_run_dir=%s", self.__class__.__name__, xeda_run_dir)
+        xeda_run_dir = Path(xeda_run_dir).resolve()
+        xeda_run_dir.mkdir(exist_ok=True, parents=True)
+        self.xeda_run_dir: Path = xeda_run_dir
+        self.cached_dependencies: bool = cached_dependencies
+        self.display_results: bool = display_results
+        self.dump_results_json: bool = dump_results_json
+        self.dump_settings_json: bool = dump_settings_json
+        self.run_in_existing_dir: bool = run_in_existing_dir
+
+    def _get_flow_run_path(
+        self,
+        design_name: str,
+        flow_name: str,
+        design_hash: Optional[str] = None,
+        flowrun_hash: Optional[str] = None,
+    ) -> Path:
+        design_subdir = design_name
+        flow_subdir = flow_name
+        if self.cached_dependencies:
+            if design_hash:
+                design_subdir += f"_{design_hash}"
+            if flowrun_hash:
+                flow_subdir += f"_{flowrun_hash}"
+
+        run_path: Path = (
+            self.xeda_run_dir / sanitize_filename(design_subdir) / flow_subdir
+        )
+        return run_path
+
+    def run_flow(
+        self,
+        flow_class: Union[str, Type[Flow]],
+        design: Design,
+        flow_settings: Union[None, Dict[str, Any], Flow.Settings] = None,
+    ) -> Flow:
+        return self._run_flow(flow_class, design, flow_settings, None)
+
+    def _run_flow(
+        self,
+        flow_class: Union[str, Type[Flow]],
+        design: Design,
+        flow_settings: Union[None, Dict[str, Any], Flow.Settings],
+        depender: Optional[Flow],
+    ) -> Flow:
+        if self.run_in_existing_dir:
+            log.error(
+                "run_in_existing_dir should only be used during Xeda's development!"
+            )
+        if isinstance(flow_class, str):
+            flow_class = get_flow_class(flow_class)
+        if flow_settings is None:
+            flow_settings = {}
+        elif isinstance(flow_settings, Flow.Settings):
+            flow_settings = asdict(flow_settings)
+        if self.debug:
+            print("flow_settings: ", flow_settings)
+        flow_settings = flow_class.Settings(**flow_settings)
+        if self.debug:
+            flow_settings.debug = True
+
+        flow_name = flow_class.name
+        # GOTCHA: design contains tb settings even for simulation flows
+        # removing tb from hash for sim flows creates a mismatch for different flows of the same design
+        design_hash = _semantic_hash(design)
+        flowrun_hash = _semantic_hash(
+            dict(
+                flow_name=flow_name,
+                flow_settings=flow_settings,
+                xeda_version=__version__,
+            ),
+        )
+        run_path = self._get_flow_run_path(
+            design.name,
+            flow_name,
+            design_hash,
+            flowrun_hash,
+        )
+
+        settings_json = run_path / "settings.json"
+        results_json = run_path / "results.json"
+
+        previous_results = None
+        if (
+            depender
+            and self.cached_dependencies
+            and run_path.exists()
+            and settings_json.exists()
+            and results_json.exists()
+        ):
+            prev_results, prev_settings = None, None
+            try:
+                with open(settings_json) as f:
+                    prev_settings = json.load(f)
+                with open(results_json) as f:
+                    prev_results = json.load(f)
+            except TypeError:
+                pass
+            except ValueError:
+                pass
+            if prev_results and prev_results.get("success") and prev_settings:
+                if (
+                    prev_settings.get("flow_name") == flow_name
+                    and prev_settings.get("design_hash") == design_hash
+                    and prev_settings.get("flowrun_hash") == flowrun_hash
+                ):
+                    previous_results = prev_results
+                else:
+                    log.warning(
+                        "%s does not contain the expected settings", prev_settings
+                    )
+            else:
+                log.warning(
+                    "Could not find valid results/settings from a previous run in %s",
+                    run_path,
+                )
+
+        if not previous_results:
+            if not self.run_in_existing_dir and run_path.exists():
+                backup_existing(run_path)
+            run_path.mkdir(parents=True)
+
+            if self.dump_settings_json:
+                log.info("dumping effective settings to %s", settings_json)
+                all_settings = dict(
+                    design=design,
+                    design_hash=design_hash,
+                    flow_name=flow_name,
+                    flow_settings=flow_settings,
+                    xeda_version=__version__,
+                    flowrun_hash=flowrun_hash,
+                )
+                dump_json(all_settings, settings_json)
+
+        with WorkingDirectory(run_path):
+            log.debug("Instantiating flow from %s", flow_class)
+            flow = flow_class(flow_settings, design, run_path)
+            flow.design_hash = design_hash
+            flow.flow_hash = flowrun_hash
+            flow.timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S")
+            # flow execution time includes init() as well as execution of all its dependency flows
+            flow.init_time = time.monotonic()
+            flow.init()
+
+        for dep_cls, dep_settings in flow.dependencies:
+            # merge with existing self.flows[dep].settings
+            # NOTE this allows dependency flow to make changes to 'design'
+            log.info(
+                "Running dependency: %s (%s.%s)",
+                dep_cls.name,
+                dep_cls.__module__,
+                dep_cls.__qualname__,
+            )
+            completed_dep = self._run_flow(dep_cls, design, dep_settings, depender=flow)
+            if not completed_dep.succeeded:
+                log.critical("Dependency flow: %s failed!", dep_cls.name)
+                raise FlowDependencyFailure()
+            flow.completed_dependencies.append(completed_dep)
+
+        flow.results["design"] = flow.design.name
+        flow.results["flow"] = flow.name
+        success = True
+
+        if previous_results:
+            log.warning("Using previous run results and artifacts from %s", run_path)
+            flow.results = previous_results
+        else:
+            with WorkingDirectory(run_path):
+                try:
+                    flow.run()
+                except NonZeroExitCode as e:
+                    log.critical(
+                        "Execution of %s returned %d", e.command_args[0], e.exit_code
+                    )
+                    success = False
+                if flow.init_time is not None:
+                    flow.results.runtime = time.monotonic() - flow.init_time
+                try:
+                    success &= flow.parse_reports()
+                except Exception as e:  # pylint: disable=broad-except
+                    log.critical("parse_reports throw an exception: %s", e)
+                    if success:  # if so far so good this is a bug!
+                        raise e from None
+                    success = False
+                if not success:
+                    log.error("Failure was reported in the parsed results.")
+                flow.results.success = success
+
+        if flow.artifacts and flow.succeeded:
+            def default_encoder(x: Any) -> str:
+                if isinstance(x, (PosixPath, os.PathLike)):
+                    return str(os.path.relpath(x, flow.run_path))
+                return str(x)
+
+            print(f"Generated artifacts in {flow.run_path}:")  # FIXME
+            print_json(data=flow.artifacts, default=default_encoder)  # FIXME
+
+        flow.results.success = success
+        if not success:
+            # set success=false if execution failed
+            log.critical("%s failed!", flow.name)
+
+        if self.dump_results_json:
+            dump_json(flow.results, results_json)
+            log.info("Results written to %s", results_json)
+
+        if self.display_results:
+            print_results(
+                flow,
+                title=f"{flow.name} Results",
+                skip_if_empty={"artifacts", "reports"},
+            )
+        return flow
+
+
+class DefaultRunner(FlowRunner):
+    """Executes a flow and its dependencies and then reports selected results"""
diff --git a/src/xeda/flows/__init__.py b/src/xeda/flows/__init__.py
new file mode 100644
index 00000000..a0c3dc31
--- /dev/null
+++ b/src/xeda/flows/__init__.py
@@ -0,0 +1,37 @@
+# all Flow classes imported here can be used from FlowRunners and will be reported on the command-line help
+from .dc import Dc
+from .diamond import DiamondSynth
+from .ghdl import GhdlSim, GhdlSynth
+from .ise import IseSynth
+from .modelsim import Modelsim
+from .nextpnr import Nextpnr
+from .openfpgaloader import Openfpgaloader
+from .quartus import Quartus
+from .vivado.vivado_synth import VivadoSynth
+from .vivado.vivado_sim import VivadoSim
+from .vivado.vivado_postsynthsim import VivadoPostsynthSim
+from .vivado.vivado_power import VivadoPower
+from .vivado.vivado_alt_synth import VivadoAltSynth
+from .yosys import Yosys
+
+# alias for backwards compatibility
+from .yosys import Yosys as YosysSynth  # pylint: disable=reimported
+
+__all__ = [
+    "Dc",
+    "DiamondSynth",
+    "GhdlSim",
+    "GhdlSynth",
+    "IseSynth",
+    "Modelsim",
+    "Nextpnr",
+    "Openfpgaloader",
+    "Quartus",
+    "VivadoAltSynth",
+    "VivadoPostsynthSim",
+    "VivadoPower",
+    "VivadoSim",
+    "VivadoSynth",
+    "Yosys",
+    "YosysSynth",
+]
diff --git a/src/xeda/flows/cocotb.py b/src/xeda/flows/cocotb.py
new file mode 100644
index 00000000..eb087902
--- /dev/null
+++ b/src/xeda/flows/cocotb.py
@@ -0,0 +1,157 @@
+import logging
+import os
+from functools import cached_property
+from pathlib import Path
+from typing import Any, Dict, List, Literal, Optional
+
+from junitparser import JUnitXml
+
+from ..dataclass import Field, XedaBaseModel, validator
+from ..design import Design
+from ..tool import Tool
+
+log = logging.getLogger(__name__)
+
+
+class CocotbSettings(XedaBaseModel):
+    coverage: bool = Field(
+        False, description="Collect coverage data if supported by simulation tool."
+    )
+    reduced_log_fmt: bool = Field(
+        True, description="Display shorter log lines in the terminal."
+    )
+    results_xml: str = Field(
+        "results.xml",
+        description="xUnit-compatible cocotb result file.",
+        hidden_from_schema=True,
+    )
+    resolve_x: Literal["VALUE_ERROR", "ZEROS", "ONES", "RANDOM"] = Field(
+        "VALUE_ERROR",
+        description="how to resolve bits with a value of X, Z, U or W when being converted to integer.",
+    )
+    testcase: List[str] = Field(
+        [],
+        description="List of test-cases to run. Can also be specified as a comma-separated string. Currently used for cocotb testbenches only.",
+    )
+    random_seed: Optional[int] = Field(
+        None,
+        description="Seed the Python random module to recreate a previous test stimulus.",
+    )
+    gpi_extra: List[str] = Field(
+        [],
+        description="A comma-separated list of extra libraries that are dynamically loaded at runtime.",
+    )
+
+    @validator("testcase", "gpi_extra", pre=True, always=True)
+    def str_to_list(cls, value):
+        if isinstance(value, str):
+            value = [s.strip() for s in value.split(",")]
+        return value
+
+
+class Cocotb(CocotbSettings, Tool):
+    """Cocotb support for a SimFlow"""
+
+    executable = "cocotb-config"
+    sim_name: str
+
+    """Not a stand-alone tool, but is used from a SimFlow"""
+
+    def vpi_path(self) -> str:
+        so_ext = "so"  # TODO windows?
+        if self.version_gte(1, 6):
+            so_path = self.run_get_stdout(
+                "--lib-name-path",
+                "vpi",
+                self.sim_name,
+            )
+        else:
+            so_path = (
+                self.run_get_stdout("--prefix")
+                + f"/cocotb/libs/libcocotbvpi_{self.sim_name}.{so_ext}"
+            )
+
+        log.info("cocotb.vpi_path: %s", so_path)
+        return so_path
+
+    def env(self, design: Design) -> Dict[str, Any]:
+        ret = {}
+        if design.tb and design.tb.cocotb:
+            assert design.tb.top, "tb.top was not set by the parent SimFlow"
+            coco_module = design.tb.sources[0].file.stem
+            tb_top_path = design.tb.sources[0].file.parent
+            ppath = []
+            current_ppath = os.environ.get("PYTHONPATH")
+            if current_ppath:
+                ppath = current_ppath.split(os.pathsep)
+            ppath.append(str(tb_top_path))
+            top: str = (
+                design.tb.top if isinstance(design.tb.top, str) else design.tb.top[0]
+            )
+            ret = {
+                "MODULE": coco_module,
+                "TOPLEVEL": top,  # TODO
+                "COCOTB_REDUCED_LOG_FMT": int(self.reduced_log_fmt),
+                "PYTHONPATH": os.pathsep.join(ppath),
+                "COCOTB_RESULTS_FILE": self.results_xml,
+                "COCOTB_RESOLVE_X": self.resolve_x,
+            }
+            if self.coverage:
+                ret["COVERAGE"] = 1
+            if self.testcase:
+                ret["TESTCASE"] = ",".join(self.testcase)
+            if self.random_seed is not None:
+                ret["RANDOM_SEED"] = self.random_seed
+            if self.gpi_extra:
+                ret["GPI_EXTRA"] = ",".join(self.gpi_extra)
+            log.info("Cocotb env: %s", ret)
+        return ret
+
+    @cached_property
+    def _results(self):
+        results_xml = self.results_xml
+        if not Path(results_xml).exists():
+            return JUnitXml()
+        return JUnitXml.fromfile(results_xml)
+
+    @property
+    def results(self):
+        return self._results
+
+    @property
+    def result_testcases(self):
+        for ts in self.results:
+            if ts is not None:
+                for tc in ts:
+                    if tc is not None:
+                        yield {
+                            "name": tc.name,
+                            "result": str(tc),
+                            "classname": tc.classname,
+                            "time": round(tc.time, 3),
+                        }
+
+    def add_results(
+        self, flow_results: Dict[str, Any], prefix: str = "cocotb."
+    ) -> bool:
+        """adds cocotb results to parent flow's results. returns success status"""
+        xml = self.results
+        flow_results[prefix + "tests"] = xml.tests
+        flow_results[prefix + "errors"] = xml.errors
+        flow_results[prefix + "failures"] = xml.failures
+        flow_results[prefix + "skipped"] = xml.skipped
+        flow_results[prefix + "time"] = xml.time
+        failed = False
+        if not xml.tests:
+            failed = True
+            log.error("No tests were discovered")
+        if xml.errors:
+            failed = True
+            log.error("Cocotb: %d error(s)", xml.errors)
+        if xml.failures:
+            failed = True
+            log.critical("Cocotb: %d failure(s)", xml.failures)
+        if failed:
+            flow_results["success"] = False
+
+        return not failed
diff --git a/src/xeda/flows/dc/__init__.py b/src/xeda/flows/dc/__init__.py
new file mode 100644
index 00000000..9a74683f
--- /dev/null
+++ b/src/xeda/flows/dc/__init__.py
@@ -0,0 +1,179 @@
+import logging
+import os
+import re
+from pathlib import Path
+from typing import Any, Dict, Mapping
+
+from box import Box
+
+from ...tool import Tool
+from ...utils import dict_merge, toml_load, try_convert
+from ..flow import AsicSynthFlow
+
+log = logging.getLogger(__name__)
+
+
+def get_hier(dct, dotted_path, default=None):
+    splitted = dotted_path.split(".")
+    merged_leaves = {}
+    for i, key in enumerate(splitted):
+        try:
+            for k, v in dct.items():
+                if not isinstance(v, Mapping):
+                    merged_leaves[k] = v
+            dct = dct[key]
+        except KeyError:
+            print(f'Key {key} not found in {".".join(splitted[:i])}!')
+            return default
+
+    for k, v in dct.items():
+        # if not isinstance(v, Mapping):
+        merged_leaves[k] = v
+    return Box(merged_leaves)
+
+
+class Dc(AsicSynthFlow):
+    dc_shell = Tool(executable="dc_shell-xg-t")  # type: ignore
+
+    class Settings(AsicSynthFlow.Settings):
+        adk: str
+
+    def run(self):
+        assert isinstance(self.settings, self.Settings)
+        ss = self.settings
+        adk_id = ss.adk
+        adk_root = Path.home() / "adk"
+        adk_config: Dict[str, Any] = {}
+        for toml_file in adk_root.glob("*.toml"):
+            adk_config = dict_merge(adk_config, toml_load(toml_file), add_new_keys=True)
+
+        adk = get_hier(adk_config, adk_id)
+        assert adk is not None
+        if not os.path.isabs(adk.path):
+            adk.path = os.path.join(str(adk_root), adk.path)
+
+        log.debug("adk=%s", adk)
+
+        script_path = self.copy_from_template(
+            "run.tcl",
+            adk=adk,
+        )
+        self.dc_shell.run("-64bit", "-topographical_mode", "-f", script_path)
+
+    def init(self) -> None:
+        self.reports_dir.mkdir(exist_ok=True)
+
+    def parse_reports(self) -> bool:
+        reports_dir = self.reports_dir
+
+        top_name = self.design.rtl.top
+        failed = False
+        self.parse_report_regex(
+            reports_dir / f"{top_name}.mapped.area.rpt",
+            r"Number of ports:\s*(?P<num_ports>\d+)",
+            r"Number of nets:\s*(?P<num_nets>\d+)",
+            r"Number of cells:\s*(?P<num_cells>\d+)",
+            r"Number of combinational cells:\s*(?P<num_cells_combinational>\d+)",
+            r"Number of sequential cells:\s*(?P<num_cells_sequentual>\d+)",
+            r"Number of macros/black boxes:\s*(?P<num_macro_bbox>\d+)",
+            r"Number of buf/inv:\s*(?P<num_buf_inv>\d+)",
+            r"Number of references:\s*(?P<num_refs>\d+)",
+            r"Combinational area:\s*(?P<area_combinational>\d+(?:\.\d+)?)",
+            r"Buf/Inv area:\s*(?P<area_buf_inv>\d+(?:\.\d+)?)",
+            r"Noncombinational area:\s*(?P<area_noncombinational>\d+(?:\.\d+)?)",
+            r"Macro/Black Box area:\s*(?P<area_macro_bbox>\d+(?:\.\d+)?)",
+            r"Net Interconnect area:\s*(?P<area_interconnect>\S+.*$)",
+            r"Total cell area:\s*(?P<area_cell_total>\d+(?:\.\d+)?)",
+            r"Total area:\s*(?P<area_macro_bbox>\w+)",
+            r"Core Area:\s*(?P<area_core>\d+(?:\.\d+)?)",
+            r"Aspect Ratio:\s*(?P<aspect_ratio>\d+(?:\.\d+)?)",
+            r"Utilization Ratio:\s*(?P<utilization_ratio>\d+(?:\.\d+)?)",
+            dotall=False,
+        )
+
+        reportfile_path = reports_dir / f"{top_name}.mapped.qor.rpt"
+
+        def parse_kvs(kvs):
+            kvs = re.split(r"\s*\n\s*", kvs)
+            kvs = [re.split(r"\s*:\s*", s.strip()) for s in kvs if s.strip()]
+            return {s[0].strip(): try_convert(s[1]) for s in kvs}
+
+        path_group_re = re.compile(
+            r"^\s*Timing Path Group\s+'(?P<path_group_name>\w+)'\n\s*\-+\s*\n(?P<kv>(?:^.*\n)+)",
+            re.MULTILINE,
+        )
+
+        area_re = re.compile(
+            r"^\s*Area\s*\n\s*\-+\s*\n(?P<kv1>(?:^.*\n)+)\s*\-+\s*\n(?P<kv2>(?:^.*\n)+)",
+            re.MULTILINE,
+        )
+        drc_re = re.compile(
+            r"^\s*Design Rules\s*\n\s*\-+\s*\n(?P<kv>(?:^.*\n)+)", re.MULTILINE
+        )
+        wns_re = re.compile(
+            r"^\s*Design\s+WNS:\s+(?P<wns>\d+\.\d+)\s+TNS:\s+(?P<tns>\d+\.\d+)\s+Number of Violating Paths:\s*(?P<nvp>\d+)"
+        )
+        hold_wns_re = re.compile(
+            r"^\s*Design\s+\(Hold\)\s+WNS:\s+(?P<wns>\d+\.\d+)\s+TNS:\s+(?P<tns>\d+\.\d+)\s+Number of Violating Paths:\s*(?P<nvp>\d+)"
+        )
+
+        # placeholder for ordering
+        self.results["path_groups"] = None
+
+        with open(reportfile_path) as rpt_file:
+            content = rpt_file.read()
+            sections = re.split(r"\n\s*\n", content)
+
+            path_groups = {}
+            for sec in sections:
+                match = path_group_re.match(sec)
+                if match:
+                    group_name = match.group("path_group_name")
+                    path_groups[group_name] = parse_kvs(match.group("kv"))
+
+                else:
+                    match = area_re.match(sec)
+                    if match:
+                        kv1 = parse_kvs(match.group("kv1"))
+                        kv2 = parse_kvs(match.group("kv2"))
+                        self.results["area"] = {**kv1, **kv2}
+                    else:
+                        match = drc_re.match(sec)
+                        if match:
+                            drc = parse_kvs(match.group("kv"))
+                            self.results["drc"] = drc
+                            if drc["Nets With Violations"] != 0:
+                                print(
+                                    f"Nets With DRC Violations: {drc['Nets With Violations']}"
+                                )
+                        else:
+                            match = wns_re.match(sec)
+                            if match:
+                                self.results["wns"] = float(match.group("wns"))
+                                self.results["tns"] = float(match.group("tns"))
+                                self.results["num_violating_paths"] = int(
+                                    match.group("nvp")
+                                )
+                                if (
+                                    self.results["wns"] > 0
+                                    or self.results["tns"] > 0
+                                    or self.results["num_violating_paths"] != 0
+                                ):
+                                    failed = True
+                            else:
+                                match = hold_wns_re.match(sec)
+                                if match:
+                                    self.results["hold_wns"] = float(match.group("wns"))
+                                    self.results["hold_tns"] = float(match.group("tns"))
+                                    self.results["hold_num_violating_paths"] = int(
+                                        match.group("nvp")
+                                    )
+                                    if (
+                                        self.results["hold_wns"] > 0
+                                        or self.results["hold_tns"] > 0
+                                        or self.results["hold_num_violating_paths"] != 0
+                                    ):
+                                        failed = True
+            self.results["path_groups"] = path_groups
+
+        return not failed
diff --git a/xeda/flows/diamond/templates/__init__.py b/src/xeda/flows/dc/templates/__init__.py
similarity index 100%
rename from xeda/flows/diamond/templates/__init__.py
rename to src/xeda/flows/dc/templates/__init__.py
diff --git a/xeda/flows/dc/templates/run.tcl b/src/xeda/flows/dc/templates/run.tcl
similarity index 94%
rename from xeda/flows/dc/templates/run.tcl
rename to src/xeda/flows/dc/templates/run.tcl
index e1cad796..18e5072e 100644
--- a/xeda/flows/dc/templates/run.tcl
+++ b/src/xeda/flows/dc/templates/run.tcl
@@ -3,14 +3,14 @@
 #
 
 set dc_design_name                {{design.rtl.top}}
-set dc_clock_period               {{flow.clock_period}} 
-set dc_topographical              {{flow.get("topographical", True)}}
+set dc_clock_period               {{settings.clock_period}} 
+set dc_topographical              {{settings.get("topographical", True)}}
 set adk_dir                       {{adk.path}}
 set dc_target_libraries           { {{- adk.target_libraries|join(' ') -}} }
 set mw_ref_libs                   "$adk_dir/{{- adk.milkeyway_reference_libraries|join(' $adk_dir/') -}}"
 set mw_tf                         $adk_dir/{{adk.milkeyway_technology_file}}
 set dc_reports_dir                {{reports_dir}}
-set dc_results_dir                {{synth_output_dir}}
+set dc_results_dir                {{settings.outputs_dir}}
 
 file mkdir ${dc_reports_dir}
 file mkdir ${dc_results_dir}
@@ -21,12 +21,12 @@ file delete -force {*}[glob -nocomplain ${dc_results_dir}/*]
 set dc_tluplus_map              {{adk.tluplus_map}}
 set dc_tluplus_max              {{adk.max_tluplus}}
 set dc_tluplus_min              {{adk.min_tluplus}}
-set dc_additional_search_path   {{flow.get('additional_search_path', '{}')}}
+set dc_additional_search_path   {{settings.get('additional_search_path', '{}')}}
 
 set_host_options -max_cores {{nthreads}}
 
 # Set up alib caching for faster consecutive runs
-set_app_var alib_library_analysis_path {{flow.alib_dir}}
+set_app_var alib_library_analysis_path {{settings.alib_dir}}
 
 # Set up search path for libraries and design files
 set_app_var search_path ". $adk_dir $dc_additional_search_path $search_path"
@@ -40,7 +40,7 @@ set_app_var synthetic_library  dw_foundation.sldb
 set_app_var link_library       [join "
                                  *
                                  $target_library
-                                 {{flow.get('extra_link_libraries',[])|join(' ')}}
+                                 {{settings.get('extra_link_libraries',[])|join(' ')}}
                                  $synthetic_library
                                "]
 
@@ -229,18 +229,18 @@ set_compile_spg_mode icc2
 
 set compile_ultra_options " -spg -retime"
 
-{% if flow.get('flatten_effort') == 0 %}
+{% if settings.get('flatten_effort') == 0 %}
 puts "Info: All design hierarchies are preserved unless otherwise specified."
 set_app_var compile_ultra_ungroup_dw false
 puts "Info: Design Compiler compile_ultra boundary optimization is disabled."
 append compile_ultra_options " -no_autoungroup -no_boundary_optimization"
-{% elif flow.get('flatten_effort') == 1 %}
+{% elif settings.get('flatten_effort') == 1 %}
 puts "Info: Unconditionally ungroup the DesignWare cells."
 set_app_var compile_ultra_ungroup_dw true
 puts "Info: Design Compiler compile_ultra automatic ungrouping is disabled."
 puts "Info: Design Compiler compile_ultra boundary optimization is disabled."
 append compile_ultra_options " -no_autoungroup -no_boundary_optimization"
-{% elif flow.get('flatten_effort') == 2 %}
+{% elif settings.get('flatten_effort') == 2 %}
 puts "Info: Unconditionally ungroup the DesignWare cells."
 set_app_var compile_ultra_ungroup_dw true
 puts "Info: Design Compiler compile_ultra automatic ungrouping is enabled."
@@ -255,7 +255,7 @@ puts "Info: Design Compiler compile_ultra boundary optimization is enabled."
 set_app_var compile_ultra_ungroup_dw true
 {% endif %}
 
-{% if flow.get('gate_clock', False) %}
+{% if settings.get('gate_clock', False) %}
 append compile_ultra_options " -gate_clock -self_gating"
 {% endif %}
 
@@ -300,7 +300,7 @@ check_design > ${dc_reports_dir}/${dc_design_name}.mapped.checkdesign.rpt
 # Synopsys Formality
 set_svf -off
 
-{% if flow.get('uniquify_with_design_name') %}
+{% if settings.get('uniquify_with_design_name') %}
 # Uniquify by prefixing every module in the design with the design name.
 # This is useful for hierarchical LVS when multiple blocks use modules
 # with the same name but different definitions.
@@ -318,9 +318,9 @@ change_names -rules verilog -hierarchy
 
 puts "\n===========================( Writing Results )==========================="
 
-{% if flow.get('saif') -%}
+{% if settings.get('saif') -%}
 # Write the .namemap file for energy analysis
-saif_map -create_map -input {{flow.saif.file}} -source_instance {{flow.saif.instance}}
+saif_map -create_map -input {{settings.saif.file}} -source_instance {{settings.saif.instance}}
 {%- endif %}
 
 # Write out files
@@ -396,8 +396,8 @@ report_reference -nosplit -hierarchy > ${dc_reports_dir}/${dc_design_name}.mappe
 report_resources -nosplit -hierarchy > ${dc_reports_dir}/${dc_design_name}.mapped.resources.rpt
 
 # Report power
-{% if flow.get('saif') -%}
-read_saif -map_names -input {{flow.saif.file}} -instance_name {{flow.saif.instance}}  -verbose
+{% if settings.get('saif') -%}
+read_saif -map_names -input {{settings.saif.file}} -instance_name {{settings.saif.instance}}  -verbose
 report_saif -hier -annotated_flag -rtl_saif > ${dc_reports_dir}/${dc_design_name}.mapped.saif.rpt
 saif_map -type ptpx -write_map ${dc_reports_dir}/${dc_design_name}.namemap
 {%- endif %}
diff --git a/src/xeda/flows/diamond/__init__.py b/src/xeda/flows/diamond/__init__.py
new file mode 100644
index 00000000..ef2324b2
--- /dev/null
+++ b/src/xeda/flows/diamond/__init__.py
@@ -0,0 +1,117 @@
+import logging
+from typing import List, Literal, Optional
+from ..flow import FpgaSynthFlow
+from ...tool import Tool
+from ...dataclass import validator
+
+log = logging.getLogger(__name__)
+
+
+class DiamondSynth(FpgaSynthFlow):
+    class Settings(FpgaSynthFlow.Settings):
+        impl_folder: str = "diamond_impl"
+        impl_name: str = "Implementation0"
+        syn_cmdline_args: Optional[List[str]] = None
+        synthesis_engine: Literal["lse", "synplify"] = "lse"
+
+        @validator("syn_cmdline_args", pre=True)
+        def validate_syn_cmdline_args(
+            cls, syn_cmdline_args: Optional[List[str]]
+        ) -> List[str]:
+            if syn_cmdline_args is None:
+                syn_cmdline_args = []
+            return syn_cmdline_args
+
+    def run(self) -> None:
+        assert isinstance(self.settings, self.Settings)
+        constraint_exts = (
+            ["ldc"] if self.settings.synthesis_engine == "lse" else ["sdc", "fdc"]
+        )
+        constraints = [f"constraints.{ext}" for ext in constraint_exts]
+        for constraint in constraints:
+            self.copy_from_template(constraint)
+        script_path = self.copy_from_template("synth.tcl")
+        diamondc = Tool("diamondc")
+        diamondc.run("diamondc", script_path)
+
+    def parse_reports(self) -> bool:
+        assert isinstance(self.settings, self.Settings)
+        reports_dir = self.run_path / "diamond_impl"
+        design_name = self.design.name
+        impl_name = self.settings.impl_name
+
+        period_pat = r"""^\s*Preference:\s+PERIOD\s+PORT\s+\"(?P<clock_port>\w+)\"\s+(?P<clock_period>\d+\.\d+)\s+ns.*HIGH\s+\d+\.\d+\s+ns\s*;\s*
+\s*\d+\s+items\s+\S+\s+(?P<_timing_errors>\d+)\s+timing\s+errors?"""
+        freq_pat = r"^\s*Preference:\s+FREQUENCY\s+PORT\s+\"(?P<clock_port>\w+)\"\s+(?P<clock_frequency>\d+\.\d+)\s+MHz\s*;\s*\n\s*\d+\s+items\s+\S+\s+(?P<_timing_errors>\d+)\s+timing\s+errors?"
+        self.parse_report_regex(
+            reports_dir / f"{design_name}_{impl_name}.twr", [period_pat, freq_pat]
+        )
+
+        if "clock_frequency" in self.results:
+            frequency = self.results["clock_frequency"]
+            period = 1000.0 / frequency
+            self.results["clock_period"] = period
+
+        else:
+            period = self.results["clock_period"]
+            frequency = 1000.0 / period
+            self.results["clock_frequency"] = frequency
+
+        slice_pat = r"^Device\s+utilization\s+summary:\s*.*^\s+SLICE\s+(?P<slice>\d+)\/(?P<_slice_avail>\d+).*^Number\s+of\s+Signals"
+        time_pat = r"""Level\s*/\s+Number\s+Worst\s+Timing\s+Worst\s+Timing\s+Run\s+NCD\s*
+\s*Cost\s+\[ncd\]\s+Unrouted\s+Slack\s+Score\s+Slack\(hold\)\s+Score\(hold\)\s+Time\s+Status\s*
+(\s*\-+){8}\s*
+\s*(?P<_lvl_cost>\S+)\s+(?P<_ncd>\S+)\s+(?P<_num_unrouted>\d+)\s+(?P<wns>\-?\d+\.\d+)\s+(?P<_setup_score>\d+)\s+(?P<whs>\-?\d+\.\d+)\s+(?P<_hold_score>\d+)\s+(?P<_runtime>\d+(?:\:\d+)*)\s+(?P<_status>\w+)\s*$"""
+        self.parse_report_regex(
+            reports_dir / f"{design_name}_{impl_name}.par", slice_pat, time_pat
+        )
+
+        # NOTE there can be "page breaks" anywhere in the mrp file (others? TODO)
+        # NOTE therefore only match lines
+        #   1. Total number of LUT4s = (Number of logic LUT4s) + 2*(Number of distributed RAMs) + 2*(Number of ripple logic)
+        #   2. Number of logic LUT4s does not include count of distributed RAM and ripple logic.
+        slice_pattern = r"""\s*Number\s+of\s+registers:\s*(?P<ff>\d+)\s+out\s+of\s*(?P<_ff_avail>\d+).*
+\s*Number\s+of\s+SLICEs:\s*(?P<_slice_map>\d+)\s*out\s+of\s*(?P<_slice_avail>\d+).*
+\s+SLICEs\s+as\s+RAM:\s*(?P<_slice_ram>\d+)\s*out\s+of\s*(?P<_slice_ram_avail>\d+).*
+\s+SLICEs\s+as\s+Carry:\s*(?P<_slice_carry>\d+)\s+out\s+of\s+(?P<_slice_carry_avail>\d+).*
+\s*Number\s+of\s+LUT4s:\s*(?P<lut>\d+)\s+out of\s+(?P<_lut_avail>\d+).*
+\s+Number\s+used\s+as\s+logic\s+LUTs:\s*(?P<_lut_logic>\d+)\s*
+\s+Number\s+used\s+as\s+distributed\s+RAM:\s*(?P<_lut_dram>\d+)\s*
+\s+Number\s+used\s+as\s+ripple\s+logic:\s*(?P<_lut_ripple>\d+)\s*
+\s+Number\s+used\s+as\s+shift\s+registers:\s*(?P<_lut_shift>\d+)\s*.*
+\s*Number\s+of\s+block\s+RAMs:\s*(?P<bram>\d+)\s+out\s+of\s+(?P<_bram_avail>\d+).*"""
+
+        dsp_pattern = r"""\s+MULT18X18D\s+(?P<_dsp_MULT18X18D>\d+)\s*.*
+\s+MULT9X9D\s+(?P<_dsp_MULT9X9D>\d+)\s*.*"""
+
+        self.parse_report_regex(
+            reports_dir / f"{design_name}_{impl_name}.mrp", slice_pattern, dsp_pattern
+        )
+
+        # FIXME add other types of available ALUs and DSPs
+        self.results["dsp"] = (
+            self.results["_dsp_MULT18X18D"] + self.results["_dsp_MULT9X9D"]
+        )
+
+        failed = False
+
+        # TODO FIXME move to LwcSynth
+        forbidden_resources = ["dsp", "bram"]
+        for res in forbidden_resources:
+            if res in self.results and self.results[res] != 0:
+                log.critical(
+                    "Map report shows %s use(s) of forbidden resource %s.",
+                    self.results[res],
+                    res,
+                )
+                failed = True
+
+        failed = (
+            failed
+            or (self.results["wns"] < 0)
+            or (self.results["whs"] < 0)
+            or (self.results["_num_unrouted"] != 0)
+            or (self.results["_status"].lower() != "completed")
+            or (self.results["_timing_errors"] != 0)
+        )
+        return not failed
diff --git a/xeda/flows/modelsim/templates/__init__.py b/src/xeda/flows/diamond/templates/__init__.py
similarity index 100%
rename from xeda/flows/modelsim/templates/__init__.py
rename to src/xeda/flows/diamond/templates/__init__.py
diff --git a/xeda/flows/diamond/templates/constraints.fdc b/src/xeda/flows/diamond/templates/constraints.fdc
similarity index 100%
rename from xeda/flows/diamond/templates/constraints.fdc
rename to src/xeda/flows/diamond/templates/constraints.fdc
diff --git a/xeda/flows/diamond/templates/constraints.ldc b/src/xeda/flows/diamond/templates/constraints.ldc
similarity index 100%
rename from xeda/flows/diamond/templates/constraints.ldc
rename to src/xeda/flows/diamond/templates/constraints.ldc
diff --git a/xeda/flows/diamond/templates/constraints.sdc b/src/xeda/flows/diamond/templates/constraints.sdc
similarity index 100%
rename from xeda/flows/diamond/templates/constraints.sdc
rename to src/xeda/flows/diamond/templates/constraints.sdc
diff --git a/xeda/flows/diamond/templates/synth.tcl b/src/xeda/flows/diamond/templates/synth.tcl
similarity index 81%
rename from xeda/flows/diamond/templates/synth.tcl
rename to src/xeda/flows/diamond/templates/synth.tcl
index 87a81773..484fb5f9 100644
--- a/xeda/flows/diamond/templates/synth.tcl
+++ b/src/xeda/flows/diamond/templates/synth.tcl
@@ -2,10 +2,10 @@
 # © 2020 [Kamyar Mohajerani](mailto:kamyar@ieee.org)
 
 # Area/Balanced/Timing
-set strategy            {{flow.strategy}}
+set strategy            {{settings.strategy}}
 
-set implementation_name "{{flow.impl_name}}"
-set impl_dir            "{{flow.impl_folder}}"
+set implementation_name "{{settings.impl_name}}"
+set impl_dir            "{{settings.impl_folder}}"
 
 file delete -force ${impl_dir}
 # Workaround for old TCL on NFS bug
@@ -13,19 +13,19 @@ file delete -force ${impl_dir}
 #   after 2000 puts "delete failed. retrying..."
 # }
 
-prj_project new -name {{design.name}} -dev "{{flow.fpga_part}}" -impl ${implementation_name} -impl_dir ${impl_dir}
+prj_project new -name {{design.name}} -dev "{{settings.fpga_part}}" -impl ${implementation_name} -impl_dir ${impl_dir}
 
-prj_impl option synthesis {{flow.synthesis_engine}}
+prj_impl option synthesis {{settings.synthesis_engine}}
 
 ##strategy
-eval prj_strgy copy -from {{flow.strategy}} -name custom_strategy -file diamond_strategy.sty
+eval prj_strgy copy -from {{settings.strategy}} -name custom_strategy -file diamond_strategy.sty
 
 {% for src in design.rtl.sources %}
 eval prj_src add {% if src.type == "vhdl" -%} -format VHDL {%- elif src.type == "verilog" and not src.variant -%} -format Verilog {%- endif %} {{src.file}}
 {% endfor %}
 
 
-{% if flow.synthesis_engine == "synplify" %}
+{% if settings.synthesis_engine == "synplify" %}
 # non-timing
 eval prj_src add constraints.fdc
 
@@ -33,7 +33,7 @@ eval prj_src add constraints.fdc
 eval prj_src add constraints.sdc
 {% endif %}
 
-{% if flow.synthesis_engine == "lse" %}
+{% if settings.synthesis_engine == "lse" %}
 eval prj_src add constraints.ldc
 {% endif %}
 
@@ -51,7 +51,7 @@ prj_strgy set_value -strategy custom_strategy map_overmap_device=False
 # prj_strgy set_value -strategy custom_strategy {maptrce_analysis_option=Standard Setup With Hold Analysis on IOs}
 # prj_strgy set_value -strategy custom_strategy {partrce_analysis_option=Standard Setup With Hold Analysis on IOs}
 
-{% if flow.strategy == "Timing" %}
+{% if settings.strategy == "Timing" %}
 prj_strgy set_value -strategy custom_strategy {syn_pipelining_retiming=Pipelining and Retiming}
 # prj_strgy set_value -strategy custom_strategy {syn_use_clk_for_uncons_io=True}
 prj_strgy set_value -strategy custom_strategy map_timing_driven=True
@@ -75,11 +75,11 @@ prj_strgy set_value -strategy custom_strategy map_timing_driven=True
 
 
 
-{% if flow.syn_cmdline_args %}
-prj_strgy set_value -strategy custom_strategy {syn_cmdline_args={{flow.syn_cmdline_args}}}
+{% if settings.syn_cmdline_args %}
+prj_strgy set_value -strategy custom_strategy {syn_cmdline_args={{settings.syn_cmdline_args|join(" ")}}}
 {% endif %}
 
-{% if flow.strategy == "Area" %}
+{% if settings.strategy == "Area" %}
 prj_strgy set_value -strategy custom_strategy syn_area=True
 {% endif %}
 
@@ -98,12 +98,12 @@ prj_strgy set_value -strategy custom_strategy par_place_effort_level=5
 
 # LSE options
 # lse_disable_distram=False ?
-{% if not flow.allow_brams %}
+{% if not settings.allow_brams %}
 prj_strgy set_value -strategy custom_strategy lse_dsp_style=Logic lse_dsp_util=0 lse_ebr_util=0 lse_rom_style=Logic
 {% endif %}
 #lse_ram_style=Distributed
 
-{% if not flow.allow_dsps %}
+{% if not settings.allow_dsps %}
 eval prj_strgy set_value -strategy custom_strategy lse_dsp_style=Logic lse_dsp_util=0
 {% endif %}
 
@@ -111,11 +111,9 @@ prj_strgy set_value -strategy custom_strategy {par_cmdline_args=-exp nbrMaxRunTi
 
 prj_strgy set custom_strategy
 
+{% if design.rtl.top -%} prj_impl option top {{design.rtl.top}} {%- endif %}
 
-
-prj_impl option top {{design.rtl.top}}
-
-prj_syn set {{flow.synthesis_engine}}
+prj_syn set {{settings.synthesis_engine}}
 
 prj_project save
 ###################
diff --git a/src/xeda/flows/flow.py b/src/xeda/flows/flow.py
new file mode 100644
index 00000000..17bfca98
--- /dev/null
+++ b/src/xeda/flows/flow.py
@@ -0,0 +1,539 @@
+"""EDA flow abstraction"""
+from __future__ import annotations
+
+import inspect
+import logging
+import multiprocessing
+import os
+import re
+from abc import ABCMeta, abstractmethod
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union
+
+from typeguard import typechecked
+import jinja2
+import psutil
+from box import Box
+from jinja2 import ChoiceLoader, PackageLoader, StrictUndefined
+
+from ..dataclass import (
+    Field,
+    ValidationError,
+    XedaBaseModel,
+    root_validator,
+    validation_errors,
+    validator,
+)
+from ..design import Design
+from ..tool import Tool
+from ..fpga import FPGA
+from ..utils import camelcase_to_snakecase, try_convert, unique
+from .cocotb import Cocotb, CocotbSettings
+
+log = logging.getLogger(__name__)
+
+
+__all__ = [
+    "Flow",
+    "FlowSettingsError",
+    "FlowFatalError",
+    "SimFlow",
+    "SynthFlow",
+    "Tool",
+]
+
+
+def regex_match(
+    string: str, pattern: str, ignorecase: bool = False
+) -> Optional[re.Match]:
+    if not isinstance(string, str):
+        return None
+    return re.match(pattern, string, flags=re.I if ignorecase else 0)
+
+
+def removesuffix(s: str, suffix: str) -> str:
+    """similar to str.removesuffix in Python 3.9+"""
+    return s[: -len(suffix)] if suffix and s.endswith(suffix) else s
+
+
+def removeprefix(s: str, suffix: str) -> str:
+    """similar to str.removeprefix in Python 3.9+"""
+    return s[len(suffix) :] if suffix and s.startswith(suffix) else s
+
+
+registered_flows: Dict[str, Tuple[str, Type["Flow"]]] = {}
+
+
+DictStrPath = Dict[str, Union[str, os.PathLike]]
+
+
+T = TypeVar("T", bound="Flow")
+
+
+@typechecked
+class Flow(metaclass=ABCMeta):
+    """A flow may run one or more tools and is associated with a single set of settings and a single design.
+    All tool executables should be available on the installed system or on the same docker image."""
+
+    name: str  # set automatically
+
+    class Settings(XedaBaseModel):
+        """Settings that can affect flow's behavior"""
+
+        verbose: int = Field(0)
+        # debug: DebugLevel = Field(DebugLevel.NONE.value, hidden_from_schema=True)
+        debug: bool = Field(False)
+        quiet: bool = Field(True)
+
+        @validator("verbose", pre=True, always=True)
+        def _validate_verbose(cls, value):
+            if isinstance(value, (bool, str)):
+                return int(value)
+            return value
+
+        @validator("quiet", pre=True, always=True)
+        def _validate_quiet(cls, value, values):
+            if values.get("verbose") or values.get("debug"):
+                return False
+            return value
+
+        timeout_seconds: int = Field(3600 * 2, hidden_from_schema=True)
+        nthreads: int = Field(
+            default_factory=multiprocessing.cpu_count,
+            description="max number of threads",
+        )
+        ncpus: int = Field(
+            psutil.cpu_count(logical=False),
+            description="Number of physical CPUs to use.",
+        )
+        no_console: bool = Field(False, hidden_from_schema=True)
+        reports_dir: str = Field("reports", hidden_from_schema=True)
+        outputs_dir: str = Field("outputs", hidden_from_schema=True)
+        clean: bool = False  # TODO remove!
+        lib_paths: List[
+            Union[
+                Tuple[
+                    str,  # library name/identifier
+                    Union[None, str, Path],  # optional library path
+                ],
+                Tuple[None, Union[str, Path]],  # or just the path
+                # both name and path can't be None
+            ]
+        ] = Field(
+            [],
+            description="Additional libraries specified as a list of (name, path) tuples. Either name or path can be none. A single string or a list of string is converted to a mapping of library names without paths",
+        )
+        dockerized: bool = Field(False, description="Run tools from docker")
+
+        @validator("lib_paths", pre=True)
+        def _lib_paths_validator(cls, value):  # pylint: disable=no-self-argument
+            if isinstance(value, str):
+                value = [(value, None)]
+            elif isinstance(value, (list, tuple)):
+                value = [(x, None) if isinstance(x, str) else x for x in value]
+            return value
+
+        def __init__(self, **data: Any) -> None:
+            try:
+                log.debug("Settings.__init__(): data=%s", data)
+                super().__init__(**data)
+            except ValidationError as e:
+                raise FlowSettingsError(
+                    validation_errors(e.errors()), e.model  # type: ignore
+                ) from e
+
+    @property
+    def succeeded(self) -> bool:
+        return self.results.success
+
+    def __init_subclass__(cls) -> None:
+        cls_name = cls.__name__
+        mod_name = cls.__module__
+        log.info("registering flow %s from %s", cls_name, mod_name)
+        cls.name = camelcase_to_snakecase(cls_name)
+        if not inspect.isabstract(cls):
+            registered_flows[cls.name] = (mod_name, cls)
+
+    def init(self) -> None:
+        """Flow custom initialization stage. At this point, more properties have been set than during __init__
+        This is usually the most appropriate place for initialization task.
+        Any dependent flows should be registered here by using add_dependency
+        """
+
+    def add_dependency(
+        self, dep_flow_class: Type["Flow"], dep_settings: Settings
+    ) -> None:
+        self.dependencies.append((dep_flow_class, dep_settings))
+
+    @classmethod
+    def _create_jinja_env(
+        cls, extra_modules: Optional[List[str]] = None
+    ) -> jinja2.Environment:
+        if extra_modules is None:
+            extra_modules = []
+        loaderChoices = []
+        mod_paths = []
+        modules = unique(
+            extra_modules + [cls.__module__] + [clz.__module__ for clz in cls.__bases__]
+        )
+        for mpx in modules:
+            for mp in [mpx, mpx.rsplit(".", 1)[0]]:
+                if mp not in mod_paths:
+                    mod_paths.append(mp)
+        for mp in mod_paths:
+            try:  # TODO better/cleaner way
+                loaderChoices.append(PackageLoader(mp))
+            except ValueError:
+                pass
+        return jinja2.Environment(
+            loader=ChoiceLoader(loaderChoices),
+            autoescape=False,
+            undefined=StrictUndefined,
+        )
+
+    def __init__(self, settings: Settings, design: Design, run_path: Path):
+        self.run_path = run_path
+        self.settings = settings
+        # assert isinstance(self.settings, self.Settings)
+        self.design: Design = design
+
+        self.init_time: Optional[float] = None
+        self.timestamp: Optional[str] = None
+        self.flow_hash: Optional[str] = None
+        self.design_hash: Optional[str] = None
+
+        # a map of "precious" files generated by the flow
+        # these files can be used in a subsequent flow depending on this flow
+        # everything else in run_path can be deleted by the flow-runner or overwritten in subsequent operations
+        # artifacts can be backed up if the same run_path is used after this flow is complete
+        self.artifacts = Box()
+        self.reports: DictStrPath = {}
+        # TODO deprecate and use self.reports
+        self.reports_dir = run_path / self.settings.reports_dir
+        self.results: Box = Box(
+            success=False,
+            # "Time of the execution of run() in fractional seconds.
+            # Initialized with None and set only after execution has finished."
+            runtime=None,
+            artifacts={},
+        )
+        self.jinja_env = self._create_jinja_env(extra_modules=[self.__module__])
+        self.add_template_test("match", regex_match)
+        self.dependencies: List[Tuple[Type[Flow], Flow.Settings]] = []
+        self.completed_dependencies: List[Flow] = []
+
+    def pop_dependency(self, typ: Type[T]) -> Flow:
+        assert inspect.isclass(typ) and issubclass(typ, Flow)
+        for i in range(len(self.completed_dependencies) - 1, -1, -1):
+            if isinstance(self.completed_dependencies[i], typ):
+                dep = self.completed_dependencies.pop(i)
+                assert isinstance(dep, typ)
+                return dep
+        raise ValueError(f"No {typ.__name__} found in completed_dependencies")
+
+    @abstractmethod
+    def run(self) -> None:
+        """return False on failure"""
+
+    def parse_reports(self) -> bool:
+        log.info("No parse_reports action for %s", self.name)
+        return True
+
+    def copy_from_template(self, resource_name, **kwargs) -> os.PathLike:
+        template = self.jinja_env.get_template(resource_name)
+        script_path: Path = self.run_path / resource_name
+        log.debug("generating %s from template.", str(script_path.resolve()))
+        rendered_content = template.render(
+            settings=self.settings,
+            design=self.design,
+            artifacts=self.artifacts,
+            **kwargs,
+        )
+        with open(script_path, "w") as f:
+            f.write(rendered_content)
+        return script_path.relative_to(self.run_path)  # resource_name
+
+    def add_template_filter(self, filter_name: str, func) -> None:
+        self.jinja_env.filters[filter_name] = func
+
+    def add_template_test(self, filter_name: str, func) -> None:
+        self.jinja_env.tests[filter_name] = func
+
+    def conv_to_relative_path(self, src):
+        path = Path(src).resolve(strict=True)
+        return os.path.relpath(path, self.run_path)
+
+    def parse_report_regex(
+        self,
+        reportfile_path: Union[str, os.PathLike],
+        re_pattern: Union[str, List[str]],
+        *other_re_patterns: Union[str, List[str]],
+        dotall: bool = True,
+        required: bool = False,
+        sequential: bool = False,
+    ) -> bool:
+        if not isinstance(reportfile_path, Path):
+            reportfile_path = Path(reportfile_path)
+        # TODO fix debug and verbosity levels!
+        if not reportfile_path.exists():
+            log.warning(
+                "File %s does not exist! Most probably the flow run had failed.\n Please check log files in %s",
+                reportfile_path,
+                self.run_path,
+            )
+            return False
+        with open(reportfile_path) as rpt_file:
+            content = rpt_file.read()
+
+            flags = re.MULTILINE | re.IGNORECASE
+            if dotall:
+                flags |= re.DOTALL
+
+            def match_pattern(pat: str, content: str) -> Tuple[bool, str]:
+                match = re.search(pat, content, flags)
+                if match is None:
+                    return False, content
+                match_dict = match.groupdict()
+                for k, v in match_dict.items():
+                    self.results[k] = try_convert(v)
+                    log.debug("%s: %s", k, self.results.get(k))
+                if sequential:
+                    content = content[match.span(0)[1] :]
+                    log.debug("len(content)=%d", len(content))
+                return True, content
+
+            for pat in [re_pattern, *other_re_patterns]:
+                matched = False
+                if isinstance(pat, list):
+                    log.debug("Matching any of: %s", pat)
+                    for subpat in pat:
+                        matched, content = match_pattern(subpat, content)
+                else:
+                    log.debug("Matching: %s", pat)
+                    matched, content = match_pattern(pat, content)
+
+                if not matched and required:
+                    log.critical(
+                        "Error parsing report file: %s\n Pattern not matched: %s\n",
+                        rpt_file.name,
+                        pat,
+                    )
+                    return False
+        return True
+
+
+class SimFlow(Flow, metaclass=ABCMeta):
+    """superclass of all simulation flows"""
+
+    cocotb_sim_name: Optional[str] = None
+
+    class Settings(Flow.Settings):
+        vcd: Optional[str] = None
+        stop_time: Union[None, str, int, float] = None
+        cocotb: CocotbSettings = (
+            CocotbSettings()
+        )  # pyright: reportGeneralTypeIssues=none
+        optimization_flags: List[str] = Field([], description="Optimization flags")
+
+        @validator("vcd", pre=True)
+        def _validate_vcd(cls, vcd):  # pylint: disable=no-self-argument
+            if vcd is not None:
+                if isinstance(vcd, bool) or not vcd:
+                    vcd = "dump.vcd" if vcd else None
+                else:
+                    assert isinstance(vcd, str)
+                    if vcd[1:].count(".") == 0:  # if it doesn't have an extension
+                        vcd += ".vcd"
+            return vcd
+
+    def __init__(self, settings: Settings, design: Design, run_path: Path):
+        super().__init__(settings, design, run_path)
+        assert isinstance(self.settings, self.Settings)
+        self.cocotb: Optional[Cocotb] = (
+            Cocotb(
+                **self.settings.cocotb.dict(),
+                sim_name=self.cocotb_sim_name,
+            )
+            if self.cocotb_sim_name
+            else None
+        )
+
+
+class TargetTechnology(XedaBaseModel):
+    liberty: Optional[str] = None
+    adk: Optional[str] = None
+    gates: Optional[str] = None
+    lut: Optional[str] = None
+
+
+class PhysicalClock(XedaBaseModel):
+    name: Optional[str] = None
+    period: float = Field(
+        description="Clock Period (in nanoseconds). Either (and only one of) 'period' OR 'freq' have to be specified."
+    )
+    freq: float = Field(
+        description="Clock frequency (in MegaHertz). Either (and only one of) 'period' OR 'freq' have to be specified."
+    )
+    rise: float = Field(0.0, description="Rise time (nanoseconds)")
+    fall: float = Field(0.0, description="Rall time (nanoseconds)")
+    uncertainty: Optional[float] = Field(None, description="Clock uncertainty")
+    skew: Optional[float] = Field(None, description="skew")
+    port: Optional[str] = Field(None, description="associated design port")
+
+    @validator("fall", always=True)
+    def fall_validate(cls, value, values):  # pylint: disable=no-self-argument
+        if not value:
+            value = round(values.get("period", 0.0) / 2.0, 3)
+        return value
+
+    @property
+    def duty_cycle(self) -> float:
+        return (self.fall - self.rise) / self.period
+
+    @property
+    def freq_mhz(self) -> float:
+        return 1000.0 / self.period
+
+    @root_validator(pre=True, skip_on_failure=True)
+    @classmethod
+    def root_validate_phys_clock(cls, values: Dict[str, Any]) -> Dict[str, Any]:
+        log.debug("%s root_validator values=%s", cls.__name__, values)
+        freq = values.get("freq")
+        if "period" in values:
+            period = float(values["period"])
+            if freq and abs(float(freq) * period - 1000.0) > 0.001:
+                raise ValueError(
+                    f"Both freq and period cannot be specified at the same time period={values.get('period')} freq={values.get('freq')}"
+                )
+            values["freq"] = 1000.0 / values["period"]
+        else:
+            if freq:
+                values["period"] = 1000.0 / float(freq)
+            else:
+                raise ValueError("Neither freq or period were specified")
+        if not values.get("name"):
+            values["name"] = "main_clock"
+        return values
+
+
+class SynthFlow(Flow, metaclass=ABCMeta):
+    """Superclass of all synthesis flows"""
+
+    class Settings(Flow.Settings):
+        """base Synthesis flow settings"""
+
+        clock_period: Optional[float] = Field(
+            None, description="target clock period in nanoseconds"
+        )
+        clocks: Dict[str, PhysicalClock] = Field({}, description="Design clocks")
+        blacklisted_resources: List[str] = []
+
+        @validator("clocks", pre=True, always=True)
+        def clocks_validate(cls, value, values):  # pylint: disable=no-self-argument
+            clock_period = values.get("clock_period")
+            if not value and clock_period:
+                value = {
+                    "main_clock": PhysicalClock(name="main_clock", period=clock_period)  # type: ignore
+                }
+            return value
+
+        @root_validator(pre=True)
+        def synthflow_settings_root_validator(cls, values):
+            clocks = values.get("clocks")
+            if clocks and "main_clock" in clocks and not values.get("clock_period"):
+                main_clock = clocks["main_clock"]
+                assert isinstance(main_clock, PhysicalClock)
+                values["clock_period"] = main_clock.period
+            return values
+
+    def __init__(self, flow_settings: Settings, design: Design, run_path: Path):
+        for clock_name, physical_clock in flow_settings.clocks.items():
+            if not physical_clock.port:
+                if clock_name not in design.rtl.clocks:
+                    raise FlowSettingsError(
+                        [
+                            (
+                                None,
+                                f"Physical clock {clock_name} has no corresponding clock port in design. Existing clocks: {', '.join(c for c in design.rtl.clocks)}",
+                                None,
+                            )
+                        ],
+                        self.Settings,
+                    )
+                physical_clock.port = design.rtl.clocks[clock_name].port
+                flow_settings.clocks[clock_name] = physical_clock
+        for clock_name, clock in design.rtl.clocks.items():
+            if clock_name not in flow_settings.clocks:
+                raise FlowSettingsError(
+                    [
+                        (
+                            None,
+                            f"No clock period or frequency was specified for clock: '{clock_name}' (clock port: '{clock.port})'",
+                            None,
+                        )
+                    ],
+                    self.Settings,
+                )
+        super().__init__(flow_settings, design, run_path)
+
+
+class FpgaSynthFlow(SynthFlow, metaclass=ABCMeta):
+    """Superclass of all FPGA synthesis flows"""
+
+    class Settings(SynthFlow.Settings):
+        """base FPGA Synthesis flow settings"""
+
+        fpga: FPGA
+
+
+class AsicSynthFlow(SynthFlow, metaclass=ABCMeta):
+    """Superclass of all ASIC synthesis flows"""
+
+    class Settings(SynthFlow.Settings):
+        """base ASIC Synthesis flow settings"""
+
+        tech: Optional[TargetTechnology] = None
+
+
+class DseFlow(Flow, metaclass=ABCMeta):
+    """Superclass of all design-space exploration flows"""
+
+
+class FlowException(Exception):
+    """Super-class of all flow exceptions"""
+
+
+class FlowDependencyFailure(FlowException):
+    """Error during execution of a dependency flow"""
+
+
+class FlowSettingsError(FlowException):
+    """Validation of settings failed"""
+
+    def __init__(
+        self,
+        errors: List[
+            Tuple[Optional[str], str, Optional[str]]
+        ],  # (location, message, type/context)
+        model,
+        *args: Any,
+    ) -> None:
+        super().__init__(*args)
+        self.errors = errors
+        self.model = model
+
+    def __str__(self) -> str:
+        return "{}: {} error{} validating {}:\n{}".format(
+            self.__class__.__qualname__,
+            len(self.errors),
+            "s" if len(self.errors) > 1 else "",
+            self.model.__qualname__,
+            "\n".join(
+                "{}{}\n".format(f"{loc}:\n   " if loc else "", msg)
+                for loc, msg, ctx in self.errors
+            ),
+        )
+
+
+class FlowFatalError(FlowException):
+    """Other fatal errors"""
diff --git a/src/xeda/flows/ghdl/__init__.py b/src/xeda/flows/ghdl/__init__.py
new file mode 100644
index 00000000..b78575de
--- /dev/null
+++ b/src/xeda/flows/ghdl/__init__.py
@@ -0,0 +1,454 @@
+import logging
+import os
+import platform
+from abc import ABCMeta
+from functools import cached_property
+from pathlib import Path
+from typing import Any, Dict, List, Literal, Optional, Union
+
+from ...dataclass import Field, validator
+from ...design import Design, DesignSource, Tuple012, VhdlSettings
+from ...gtkwave import gen_gtkw
+from ...tool import Docker, Tool
+from ...utils import SDF, common_root, setting_flag
+from ..flow import Flow, FlowSettingsError, SimFlow, SynthFlow
+
+log = logging.getLogger(__name__)
+
+
+def _get_wave_opt_signals(wave_opt_file, extra_top=None):
+    signals = []
+    with open(wave_opt_file, "r") as f:
+        for line in f.read().splitlines():
+            line = line.strip()
+            if line.startswith("/"):
+                sig = line[1:].split("/")
+                if extra_top:
+                    sig.insert(0, extra_top)
+                signals.append(sig)
+    root_group = common_root(signals)
+    return signals, root_group
+
+
+class GhdlTool(Tool):
+    """GHDL VHDL simulation, synthesis, and linting tool: https://ghdl.readthedocs.io"""
+
+    docker = Docker(image="hdlc/sim:osvb")
+    executable = "ghdl"
+
+    @cached_property
+    def _info(self) -> Dict[str, str]:
+        out = self.run_get_stdout(
+            "--version",
+        )
+        lines = [line.strip() for line in out.splitlines()]
+        if len(lines) < 3:
+            return {}
+        self._version = tuple(lines[0].split("."))
+
+        return {
+            "version": ".".join(self.version),
+            "compiler": lines[1],
+            "backend": lines[2],
+        }
+
+
+class Ghdl(Flow, metaclass=ABCMeta):
+    """VHDL simulation using GHDL"""
+
+    ghdl = GhdlTool()  # pyright: reportGeneralTypeIssues=none
+
+    class Settings(Flow.Settings):
+        analysis_flags: List[str] = []
+        elab_flags: List[str] = ["--syn-binding"]
+        warn_flags: List[str] = [
+            "--warn-binding",
+            "--warn-default-binding",
+            "--warn-reserved",
+            "--warn-library",
+            "--warn-vital-generic",
+            "--warn-shared",
+            "--warn-runtime-error",
+            "--warn-body",
+            "--warn-specs",
+            "--warn-unused",
+            "--warn-static",
+            "--warn-parenthesis",
+        ]
+        werror: bool = Field(
+            False, description="warnings are always considered as errors"
+        )
+        elab_werror: bool = Field(
+            False, description="During elaboration, warnings are considered as errors"
+        )
+        relaxed: bool = Field(
+            True,
+            description="Slightly relax some rules to be compatible with various other simulators or synthesizers.",
+        )
+        clean: bool = Field(False, description="Run 'clean' before elaboration")
+        diagnostics: bool = Field(
+            True, description="Enable both color and source line carret diagnostics."
+        )
+        work: Optional[str] = Field(
+            None, description="Set the name of the WORK library"
+        )
+        expect_failure: bool = False
+
+        def common_flags(self, vhdl: VhdlSettings) -> List[str]:
+            cf: List[str] = []
+            if vhdl.standard:
+                cf.append(f"--std={vhdl.standard}")
+            if vhdl.synopsys:
+                cf.append("-fsynopsys")
+            if self.work:
+                cf.append(f"--work={self.work}")
+            cf += [f"-P{p}" for p in self.lib_paths]
+            return cf
+
+        @staticmethod
+        def generics_flags(generics: Optional[Dict[str, Any]]) -> List[str]:
+            if not generics:
+                return []
+
+            def conv(v):
+                if isinstance(v, bool):
+                    return str(v).lower()
+                return v
+
+            return [f"-g{k}={conv(v)}" for k, v in generics.items()]
+
+        def get_flags(self, vhdl: VhdlSettings, stage: str) -> List[str]:
+            common = self.common_flags(vhdl)
+            warn_flags: List[str] = self.warn_flags
+            analysis_flags: List[str] = common + self.analysis_flags
+            elab_flags: List[str] = common + self.elab_flags
+            find_top_flags = common
+            if stage == "common":
+                return common
+            if stage == "remove":
+                return common
+            if self.werror:
+                warn_flags += ["--warn-error"]
+            elif self.elab_werror:
+                elab_flags += ["--warn-error"]
+            if self.relaxed:
+                analysis_flags.extend(["-frelaxed-rules", "-frelaxed", "--mb-comments"])
+                elab_flags.extend(["-frelaxed"])
+            if self.verbose:
+                analysis_flags.append("-v")
+                elab_flags.append("-v")
+            if self.diagnostics:
+                elab_flags.extend(
+                    [
+                        "-fcaret-diagnostics",
+                        "-fcolor-diagnostics",
+                        "-fdiagnostics-show-option",
+                    ]
+                )
+            if self.work:
+                elab_flags.append("-v")
+            if self.expect_failure:
+                elab_flags.append("--expect-failure")
+            if vhdl.synopsys:
+                analysis_flags.append("--ieee=synopsys")
+            if stage in ("import", "analyze"):
+                return analysis_flags + warn_flags
+            if stage in ("make", "elaborate"):
+                return elab_flags + warn_flags
+            if stage in ("find-top"):
+                return find_top_flags
+            raise ValueError("unknown stage!")
+
+    def elaborate(
+        self, sources: List[DesignSource], top: Union[str, Tuple012], vhdl: VhdlSettings
+    ) -> Tuple012:
+        """returns top(s) as a list"""
+        assert isinstance(self.settings, self.Settings)
+        ss = self.settings
+        if isinstance(top, str):
+            top = (top,)
+        steps = ["import", "make"]
+        if ss.clean:
+            steps.insert(0, "remove")
+        if not top:
+            # run find-top after import
+            log.warning("added find-top to steps")
+            steps.insert(steps.index("import") + 1, "find-top")
+        for step in steps:
+            args = ss.get_flags(vhdl, step)
+            if isinstance(ss, SimFlow.Settings):
+                args += ss.optimization_flags
+            if step in ["import", "analyze"]:
+                args += [str(s) for s in sources]
+            elif step in ["make", "elaborate"]:
+                if self.ghdl.info.get("backend", "").lower().startswith("llvm"):
+                    if platform.system() == "Darwin" and platform.machine() == "arm64":
+                        args += ["-Wl,-Wl,-no_compact_unwind"]
+                args += list(top)
+            if step == "find-top":
+                out = self.ghdl.run_get_stdout(step, *args)
+                top_list = out.strip().split()
+                # clunky way of converting to tuple, just to be safe and also keep mypy happy
+                top = (
+                    ()
+                    if len(top_list) == 0
+                    else (top_list[0],)
+                    if len(top_list) == 1
+                    else (top_list[0], top_list[1])
+                )
+                if top:
+                    log.info("find-top: top-module was set to %s", top)
+                else:
+                    log.warning("find-top: unable to determine the top-module")
+            else:
+                self.ghdl.run(step, *args)
+        return top
+
+
+class GhdlSynth(Ghdl, SynthFlow):
+    """
+    Convert a VHDL design using 'ghdl --synth'
+     (Please take a look at 'Yosys' flow (or other synthesis flows) for general VHDL, Verilog, or mixed-language synthesis targeting FPGAs or ASICs)
+    """
+
+    class Settings(Ghdl.Settings, SynthFlow.Settings):
+        vendor_library: Optional[str] = Field(
+            None, description="Any unit from this library is a black box"
+        )
+        no_formal: bool = Field(
+            True,
+            description="Neither synthesize assert nor PSL. Required for yosys+nextpnr flow.",
+        )
+        no_assert_cover: bool = Field(
+            False, description="Cover PSL assertion activation"
+        )
+        assert_assumes: bool = Field(
+            False, description="Treat all PSL asserts like PSL assumes"
+        )
+        assume_asserts: bool = Field(
+            False, description="Treat all PSL assumes like PSL asserts"
+        )
+        out: Optional[
+            Literal["vhdl", "raw-vhdl", "verilog", "dot", "none", "raw", "dump"]
+        ] = Field(None, description="Type of output to generate")
+        out_file: Optional[str] = None
+
+    def run(self) -> None:
+        design = self.design
+        assert isinstance(self.settings, self.Settings)
+        ss = self.settings
+        top = self.elaborate(design.rtl.sources, design.rtl.top, design.language.vhdl)
+        args = self.synth_args(ss, design, one_shot_elab=False, top=top)
+        self.ghdl.run_stdout_to_file("synth", *args, stdout=ss.out_file)
+
+    @staticmethod
+    def synth_args(
+        ss: Settings, design: Design, one_shot_elab: bool = True, top: Tuple012 = ()
+    ) -> List[str]:
+        flags = ss.get_flags(design.language.vhdl, "elaborate")
+        flags += setting_flag(ss.vendor_library)
+        flags += setting_flag(ss.out)
+        flags += setting_flag(ss.no_formal)
+        flags += setting_flag(ss.no_assert_cover)
+        flags += setting_flag(ss.assert_assumes)
+        flags += setting_flag(ss.assume_asserts)
+
+        flags.extend(ss.generics_flags(design.rtl.generics))
+        if one_shot_elab:
+            flags += [str(v) for v in design.rtl.sources if v.type == "vhdl"]
+            flags.append("-e")
+        if (
+            design.rtl.sources[-1].type == "vhdl"
+        ):  # add top if last source (top source) is VHDL
+            if not top:
+                top = (design.rtl.top,)
+            flags.extend(list(top))
+        return flags
+
+
+# class GhdlLint(Ghdl, Flow):
+#     """Lint VHDL sources using GHDL"""
+
+#     class Settings(Ghdl.Settings):
+#         pass
+
+
+class GhdlSim(Ghdl, SimFlow):
+    """Simulate a VHDL design using GHDL"""
+
+    cocotb_sim_name = "ghdl"
+
+    class Settings(Ghdl.Settings, SimFlow.Settings):
+        run_flags: List[str] = []
+        optimization_flags: List[str] = Field(
+            ["-O3"], description="Simulation optimization flags"
+        )
+        asserts: Optional[Literal["disable", "disable-at-0"]] = None
+        ieee_asserts: Optional[Literal["disable", "disable-at-0"]] = "disable-at-0"
+        sdf: SDF = Field(
+            SDF(),
+            description="Do VITAL annotation using SDF files(s). A single string is interpreted as a MAX SDF file.",
+        )
+        wave: Optional[str] = Field(
+            None, description="Write the waveforms into a GHDL Waveform (GHW) file."
+        )
+        read_wave_opt: Optional[str] = Field(
+            None,
+            description="Filter signals to be dumped to the wave file according to the wave option file provided.",
+        )
+        write_wave_opt: Optional[str] = Field(
+            None,
+            description="Creates a wave option file with all the signals of the design. Overwrites the file if it already exists.",
+        )
+        fst: Optional[str] = Field(
+            None, description="Write the waveforms into an _fst_ file."
+        )
+        stop_delta: Optional[str] = Field(
+            None,
+            description="Stop the simulation after N delta cycles in the same current time.",
+        )
+        disp_tree: Optional[Literal["inst", "proc", "port"]] = Field(
+            None,
+            description="Display the design hierarchy as a tree of instantiated design entities. See GHDL documentation for more details.",
+        )
+        vpi: Union[None, str, List[str]] = Field(
+            None,
+            description="Load VPI library (or multiple libraries)",
+        )
+        # TODO workdir?
+
+        @validator("wave", "fst", pre=True)
+        def validate_wave(cls, value, field):  # pylint: disable=no-self-argument
+            if value is not None:
+                ext = (
+                    ".ghw"
+                    if field.name == "wave"
+                    else ".fst"
+                    if field.name == "fst"
+                    else ""
+                )
+                if isinstance(value, bool):
+                    value = "dump" + ext if value else None
+                else:
+                    if not isinstance(value, str):
+                        value = str(value)
+                    if not value.endswith(ext):
+                        value += ext
+            return value
+
+    def run(self) -> None:
+        design = self.design
+        assert design.tb
+        assert isinstance(self.settings, self.Settings)
+        ss = self.settings
+        cf = ss.common_flags(design.language.vhdl)
+        run_flags = self.settings.run_flags
+        sdf_root = ss.sdf.root if ss.sdf.root else design.tb.uut
+        for delay_type, sdf_file in ss.sdf.delay_items():
+            if sdf_file:
+                assert sdf_root, "neither SDF root nor tb.uut are provided"
+                run_flags.append(f"--sdf={delay_type}={sdf_root}={sdf_file}")
+
+        def fp(s: Union[str, os.PathLike]) -> str:
+            if not os.path.isabs(s):
+                return str(self.run_path.relative_to(Path.cwd()) / s)
+            return str(s)
+
+        if ss.vcd:
+            if ss.vcd.endswith((".gz", ".vcdgz")):
+                run_flags.append(f"--vcdgz={ss.vcd}")
+            else:
+                run_flags.append(f"--vcd={ss.vcd}")
+            log.warning("Dumping VCD to %s", fp(ss.vcd))
+        if ss.fst:
+            run_flags += setting_flag(ss.fst)
+            log.warning("Dumping fst to %s", fp(ss.fst))
+        if ss.wave:
+            run_flags += setting_flag(ss.wave)
+            log.warning("Dumping GHW to %s", fp(ss.wave))
+
+        if ss.wave or ss.vcd or ss.fst:
+            if not ss.read_wave_opt and not ss.write_wave_opt:
+                ss.write_wave_opt = "wave.opt"
+
+        if ss.write_wave_opt:
+            p = Path(ss.write_wave_opt)
+            if p.exists():
+                log.warning("Deleting existing wave option file: %s", p)
+                p.unlink()
+        run_flags += setting_flag(ss.write_wave_opt)
+        if ss.read_wave_opt:
+            if not Path(ss.read_wave_opt).exists():  # TODO move to validation
+                raise FlowSettingsError(
+                    [
+                        (
+                            "read_wave_opt",
+                            f"File {ss.read_wave_opt} does not exist",
+                            None,
+                        )
+                    ],
+                    self.Settings,
+                )
+        run_flags += setting_flag(ss.read_wave_opt)
+        run_flags += setting_flag(ss.disp_tree)
+        run_flags += setting_flag(ss.asserts)
+        run_flags += setting_flag(ss.ieee_asserts)
+
+        vpi = (
+            []
+            if ss.vpi is None
+            else [ss.vpi]
+            if not isinstance(ss.vpi, (list, tuple))
+            else list(ss.vpi)
+        )
+        # TODO factor out cocotb handling
+        if design.tb.cocotb and self.cocotb:
+            vpi.append(self.cocotb.vpi_path())
+            # tb_generics = list(design.tb.generics)  # TODO pass to cocotb?
+            design.tb.generics = design.rtl.generics
+        run_flags += setting_flag(vpi)
+
+        if ss.debug:
+            run_flags.extend(
+                [
+                    "--trace-processes",
+                    "--checks",
+                ]
+            )
+
+        run_flags += setting_flag(ss.stop_time)
+        run_flags += setting_flag(ss.stop_delta)
+
+        run_flags.extend(ss.generics_flags(design.tb.generics))
+
+        x = self.elaborate(design.sim_sources, design.tb.top, design.language.vhdl)
+        design.tb.top = x
+        assert self.cocotb
+        self.ghdl.run(
+            "run",
+            *cf,
+            *design.sim_tops,
+            *run_flags,
+            env=self.cocotb.env(design),
+        )
+
+    def parse_reports(self) -> bool:
+        success = True
+        assert isinstance(self.settings, self.Settings)
+        ss = self.settings
+
+        dump_file = ss.wave or ss.vcd or ss.fst
+        if dump_file:
+            log.debug("Generating GtkWave save-file form dump_file=%s", dump_file)
+            opt_file = ss.read_wave_opt or ss.write_wave_opt
+            extra_top = "top" if ss.wave else None
+            signals, root_group = _get_wave_opt_signals(opt_file, extra_top)
+            if dump_file == ss.fst:  # fst removes the common hierarchy
+                signals = [s[len(root_group) :] for s in signals]
+                root_group = []
+            gen_gtkw(dump_file, signals, root_group)
+
+        # TODO move
+        if self.cocotb and self.design.tb and self.design.tb.cocotb:
+            success &= self.cocotb.add_results(self.results)
+        return success
diff --git a/src/xeda/flows/ise/__init__.py b/src/xeda/flows/ise/__init__.py
new file mode 100644
index 00000000..d32c92ab
--- /dev/null
+++ b/src/xeda/flows/ise/__init__.py
@@ -0,0 +1,73 @@
+"""Xilinx ISE Synthesis flow"""
+import logging
+from typing import Mapping, Union
+
+from ...tool import Tool
+from ...utils import try_convert
+from ..flow import FpgaSynthFlow
+
+logger = logging.getLogger(__name__)
+
+OptionValueType = Union[str, int, bool, float]
+OptionsType = Mapping[str, OptionValueType]
+
+
+class IseSynth(FpgaSynthFlow):
+    """FPGA synthesis using Xilinx ISE"""
+
+    class Settings(FpgaSynthFlow.Settings):
+        # see https://www.xilinx.com/support/documentation/sw_manuals/xilinx14_7/devref.pdf
+        synthesis_options: OptionsType = {
+            "Optimization Effort": "High",
+            "Optimization Goal": "Speed",
+            "Keep Hierarchy": "No",
+        }
+        map_options: OptionsType = {
+            "Global Optimization": "Speed",
+            "Map Effort Level": "High",
+            "Placer Effort Level": "High",
+            "Perform Timing-Driven Packing and Placement": True,
+            "Combinatorial Logic Optimization": True,
+        }
+        pnr_options: OptionsType = {
+            "Place & Route Effort Level (Overall)": "High",
+        }
+        trace_options: OptionsType = {
+            "Report Type": "Verbose Report",
+        }
+
+    def run(self) -> None:
+        xcf_file = self.copy_from_template("constraints.xcf")
+        ucf_file = self.copy_from_template("constraints.ucf")
+
+        self.add_template_filter(
+            "quote_str", lambda v: f'"{v}"' if isinstance(v, str) else v
+        )
+
+        script_path = self.copy_from_template(
+            "ise_synth.tcl",
+            xcf_file=xcf_file,
+            ucf_file=ucf_file,
+        )
+        xtclsh = Tool("xtclsh")
+        xtclsh.run(script_path)
+
+    def parse_reports(self) -> bool:
+        top = self.design.rtl.top
+        assert top
+        # self.parse_report_regex(self.design.name + ".twr", r'(?P<wns>\-?\d+')
+        fail = not self.parse_report_regex(
+            top + "_par.xrpt",
+            r'stringID="PAR_SLICES" value="(?P<slice>\-?\d+)"',
+            r'stringID="PAR_SLICE_REGISTERS" value="(?P<ff>\-?\d+)"',
+            r'stringID="PAR_SLICE_LUTS" value="(?P<lut>\-?\d+)"',
+        )
+        fail |= not self.parse_report_regex(
+            top + ".syr",
+            r"Minimum period:\s+(?P<minimum_period>\-?\d+(?:\.\d+)?)ns\s+\(Maximum Frequency: (?P<maximum_frequency>\-?\d+(?:\.\d+)?)MHz\)",
+            r"Slack:\s+(?P<wns>\-?\d+(?:\.\d+)?)ns",
+        )
+        if "wns" in self.results:
+            wns = try_convert(self.results["wns"])
+            fail |= not isinstance(wns, (float, int)) or wns < 0
+        return not fail
diff --git a/src/xeda/flows/ise/templates/constraints.ucf b/src/xeda/flows/ise/templates/constraints.ucf
new file mode 100644
index 00000000..ad21dc64
--- /dev/null
+++ b/src/xeda/flows/ise/templates/constraints.ucf
@@ -0,0 +1,2 @@
+NET "{{design.rtl.clock_port}}" TNM_NET="{{design.rtl.clock_port}}";
+TIMESPEC "TS{{design.rtl.clock_port}}" = PERIOD "clock" {{settings.clock_period}} ns;
\ No newline at end of file
diff --git a/src/xeda/flows/ise/templates/constraints.xcf b/src/xeda/flows/ise/templates/constraints.xcf
new file mode 100644
index 00000000..2503bd56
--- /dev/null
+++ b/src/xeda/flows/ise/templates/constraints.xcf
@@ -0,0 +1,2 @@
+NET "{{design.rtl.clock_port}}" TNM_NET = "{{design.rtl.clock_port}}";
+TIMESPEC "TS{{design.rtl.clock_port}}" = PERIOD "{{design.rtl.clock_port}}" {{settings.clock_period}} ns HIGH 50%;
\ No newline at end of file
diff --git a/src/xeda/flows/ise/templates/ise_synth.tcl b/src/xeda/flows/ise/templates/ise_synth.tcl
new file mode 100644
index 00000000..e26932e1
--- /dev/null
+++ b/src/xeda/flows/ise/templates/ise_synth.tcl
@@ -0,0 +1,57 @@
+
+## Automatically generated by Xeda
+## ISE 14.7
+
+puts "\n==============================( Creating Project )================================"
+if { [catch  { project new {{design.name}} }] } {
+    project open {{design.name}}
+}
+
+project set family {{settings.fpga.family}}
+project set device {{settings.fpga.device}}
+project set package {{settings.fpga.package}}
+project set speed {{settings.fpga.speed}}
+project set "Generate Detailed MAP Report" TRUE
+
+project set "Auto Implementation Top" true
+
+puts "\n==============================( Adding RTL Sources )================================"
+{% for src in design.rtl.sources %}
+xfile add {{src.file}} -copy
+{%- endfor %}
+
+project set top {{design.rtl.top}}
+
+puts "\n==============================( Adding Constraint Files )================================"
+if { [catch  { xfile add {{ucf_file}} -copy }] } {
+    puts "unable to add {{ucf_file}}"
+}
+
+project set "Use Synthesis Constraints File" TRUE
+project set "Synthesis Constraints File" "{{xcf_file}}"
+
+
+puts "\n==============================( Setting Options )================================"
+project set "Enable Multi-Threading" {{2 if settings.nthreads and settings.nthreads > 1 else "Off"}} -process "MAP"
+project set "Enable Multi-Threading" {{[settings.nthreads, 4]|min if settings.nthreads and settings.nthreads > 1 else "Off"}} -process "Place & Route"
+
+{% for k, v in settings.synthesis_options.items() %}
+project set "{{k}}" {{v|quote_str}} -process "Synthesize - XST"
+{% endfor %}
+{% for k, v in settings.map_options.items() %}
+project set "{{k}}" {{v|quote_str}} -process "MAP"
+{% endfor %}
+{% for k, v in settings.pnr_options.items() %}
+project set "{{k}}" {{v|quote_str}} -process "Place & Route"
+{% endfor %}
+{% for k, v in settings.trace_options.items() %}
+project set "{{k}}" {{v|quote_str}} -process "Generate Post-Place & Route Static Timing"
+{% endfor %}
+
+
+puts "\n==============================( Implementing Design )================================"
+
+process run "Implement Design"
+
+# project close
+
diff --git a/src/xeda/flows/modelsim/__init__.py b/src/xeda/flows/modelsim/__init__.py
new file mode 100644
index 00000000..047fd61d
--- /dev/null
+++ b/src/xeda/flows/modelsim/__init__.py
@@ -0,0 +1,42 @@
+# © 2020 [Kamyar Mohajerani](mailto:kamyar@ieee.org)
+
+from typing import Optional
+from ...utils import SDF
+from ..flow import SimFlow
+from ...tool import Tool
+
+
+class Modelsim(SimFlow):
+    class Settings(SimFlow.Settings):
+        sdf: SDF = SDF()
+        modelsimini: Optional[str] = None
+
+    def run(self) -> None:
+        assert isinstance(self.settings, self.Settings)
+        vcom_options = ["-lint"]
+        vlog_options = ["-lint"]
+        vsim_opts = []
+        tb = self.design.tb
+        ss = self.settings
+        # TODO are library paths supported?
+        vsim_opts.extend([f"-L {lib_name}" for lib_name in ss.lib_paths])
+        sdf_root = ss.sdf.root if ss.sdf.root else tb.uut
+        for dt, f in ss.sdf.delay_items():
+            assert sdf_root, "Neither settings.sdf.root or design.tb.uut are provided"
+            vsim_opts.extend([f"-sdf{dt}", f"{sdf_root}={f}"])
+
+        tb_generics_opts = " ".join([f"-g{k}={v}" for k, v in tb.parameters.items()])
+
+        script_path = self.copy_from_template(
+            "run.tcl",
+            generics_options=tb_generics_opts,
+            vcom_opts=" ".join(vcom_options),
+            vlog_opts=" ".join(vlog_options),
+            vsim_opts=" ".join(vsim_opts),
+        )
+
+        modelsim_opts = ["-batch", "-do", f"do {script_path}"]
+        if ss.modelsimini:
+            modelsim_opts.extend(["-modelsimini", ss.modelsimini])
+        vsim = Tool("vsim")
+        vsim.run(*modelsim_opts)
diff --git a/xeda/flows/vivado/templates/__init__.py b/src/xeda/flows/modelsim/templates/__init__.py
similarity index 100%
rename from xeda/flows/vivado/templates/__init__.py
rename to src/xeda/flows/modelsim/templates/__init__.py
diff --git a/xeda/flows/modelsim/templates/run.tcl b/src/xeda/flows/modelsim/templates/run.tcl
similarity index 69%
rename from xeda/flows/modelsim/templates/run.tcl
rename to src/xeda/flows/modelsim/templates/run.tcl
index 65932b09..51d1f6ab 100644
--- a/xeda/flows/modelsim/templates/run.tcl
+++ b/src/xeda/flows/modelsim/templates/run.tcl
@@ -1,36 +1,35 @@
 puts "\n===========================( Compiling HDL Sources )==========================="
-{% for src in (design.rtl.sources + design.tb.sources) if src.type %}
-{% if src.type == 'verilog' %}
+{% for src in design.sim_sources if src.type -%}
+{% if src.type == 'verilog' -%}
 if { [catch {eval vlog {{src.file}} {% if src.variant == "systemverilog" -%} -sv {%- endif -%} {{vlog_opts}} } error]} {
     puts $error
     exit 1
 }
-
-{% elif src.type == 'vhdl' %}
+{% elif src.type == 'vhdl' -%}
 if { [catch {eval vcom {{src.file}} {{vcom_opts}} {%- if design.language.vhdl.standard == "08" %} -2008 {% elif design.language.vhdl.standard == "02" %} -2002 {% elif design.language.vhdl.standard == "93" %} -93 {% endif -%} } error]} {
     puts $error
     exit 1
 }
-{% endif %}
-{% endfor %}
+{%- endif %}
+{%- endfor %}
 
 puts "\n===========================( Running simulation )==========================="
 
-{% if vcd %}
-vcd file {{vcd}}
+{% if settings.vcd %}
+vcd file {{settings.vcd}}
 {% endif %}
 
 puts "\n===========================( *ENABLE ECHO* )==========================="
-if { [catch {eval vsim -t ps {{top}} {{vsim_opts}} {{generics_options}} } error]} {
+if { [catch {eval vsim -t ps {{design.sim_tops|join(' ')}} {{vsim_opts}} {{generics_options}} } error]} {
     puts $error
     exit 1
 }
 vcd add -r {% if not debug and design.tb.uut %} {{design.tb.uut}}/* {% else %} * {% endif %}
 #run_wave
-run {% if 'stop_time' in flow %} {{flow.stop_time}} {%- else %} -all {%- endif %}
+run {% if 'stop_time' in flow %} {{settings.stop_time}} {%- else %} -all {%- endif %}
 puts "\n===========================( *DISABLE ECHO* )==========================="
 
-{% if vcd %}
+{% if settings.vcd %}
 vcd flush
 {% endif %}
 
diff --git a/src/xeda/flows/nextpnr.py b/src/xeda/flows/nextpnr.py
new file mode 100644
index 00000000..044cae8a
--- /dev/null
+++ b/src/xeda/flows/nextpnr.py
@@ -0,0 +1,235 @@
+import logging
+from typing import Optional, List, Union
+from urllib.request import urlretrieve
+from urllib.parse import urlparse
+from urllib.error import HTTPError
+
+from .yosys import Yosys
+from .flow import FlowFatalError, FpgaSynthFlow
+from ..tool import Tool
+from ..utils import setting_flag
+from ..dataclass import Field, XedaBaseModel, validator
+from ..board import WithFpgaBoardSettings, get_board_file_path, get_board_data
+
+__all__ = ["Nextpnr"]
+
+log = logging.getLogger(__name__)
+
+
+class EcpPLL(Tool):
+    class Clock(XedaBaseModel):
+        name: Optional[str] = None
+        mhz: float = Field(gt=0, description="frequency in MHz")
+        phase: Optional[float] = None
+
+    executable = "ecppll"
+    module: str = "ecp5pll"
+    reset: bool = False
+    standby: bool = False
+    highres: bool = False
+    internal_feedback: bool = False
+    internal_feedback_wake: bool = False
+    clkin: Union[Clock, float] = 25.0
+    clkouts: List[Union[Clock, float]]
+    file: Optional[str] = None
+
+    @classmethod
+    def _fix_clock(cls, clock, out_clk=None) -> Clock:
+        if isinstance(clock, float):
+            clock = cls.Clock(mhz=clock)
+        if not clock.name:
+            clock.name = "clk_i" if out_clk is None else f"clk_o_{out_clk}"
+        return clock
+
+    @validator("clkin", always=True)  # type: ignore
+    def _validate_clockin(cls, value):
+        return cls._fix_clock(value)
+
+    @validator("clkouts", always=True)  # type: ignore
+    def _validate_clockouts(cls, value):
+        new_value = []
+        for i, v in enumerate(value):
+            new_value.append(cls._fix_clock(v, i))
+        return new_value
+
+    @validator("file", always=True)
+    def _validate_outfile(cls, value, values):
+        if not value:
+            value = values["module"] + ".v"
+        return value
+
+    def generate(self):
+        def s(f: float) -> str:
+            return f"{f:0.03f}"
+
+        args = setting_flag(self.module)
+        args += setting_flag(self.file)
+        args += ["--clkin_name", self.clkin.name]  # type: ignore # pylint: disable=no-member
+        args += ["--clkin", s(self.clkin.mhz)]  # type: ignore # pylint: disable=no-member
+        if not 1 <= len(self.clkouts) <= 4:
+            raise ValueError("At least 1 and at most 4 output clocks can be specified")
+        for i, clk in enumerate(self.clkouts):
+            assert isinstance(clk, self.Clock) and clk.name
+            args += [f"--clkout{i}_name", clk.name]
+            args += [f"--clkout{i}", s(clk.mhz)]
+            if clk.phase:
+                if i > 0:
+                    args += [f"--phase{i}", s(clk.phase)]
+                else:
+                    raise ValueError(
+                        "First output clock cannot have a phase difference!"
+                    )
+        args += setting_flag(self.highres)
+        args += setting_flag(self.standby)
+        args += setting_flag(self.internal_feedback)
+        self.run(*args)
+
+
+class Nextpnr(FpgaSynthFlow):
+    class Settings(WithFpgaBoardSettings):
+        verbose: bool = False
+        lpf_cfg: Optional[str] = None
+        seed: Optional[int] = None
+        randomize_seed: bool = False
+        timing_allow_fail: bool = False
+        ignore_loops: bool = Field(
+            False, description="ignore combinational loops in timing analysis"
+        )
+
+        textcfg: Optional[str] = "config.txt"
+        out_of_context: bool = Field(
+            False,
+            description="disable IO buffer insertion and global promotion/routing, for building pre-routed blocks",
+        )
+        lpf_allow_unconstrained = Field(
+            False,
+            description="don't require LPF file(s) to constrain all IOs",
+        )
+        extra_args: List[str] = []
+        py_script: Optional[str] = None
+        write: Optional[str] = None
+        sdf: Optional[str] = None
+        log: Optional[str] = "nextpnr.log"
+        report: Optional[str] = "report.json"
+        detailed_timing_report: bool = False  # buggy and likely to segfault
+        placed_svg: Optional[str] = None  # "placed.svg"
+        routed_svg: Optional[str] = None  # "routed.svg"
+        parallel_refine: bool = False
+        yosys: Optional[Yosys.Settings] = None
+
+        @validator("yosys", always=True, pre=False)
+        def _validate_yosys(cls, value, values):
+            clocks = values.get("clocks")
+            fpga = values.get("fpga")
+            if value is None:
+                value = Yosys.Settings(
+                    fpga=fpga,
+                    clocks=clocks,
+                )  # pyright: reportGeneralTypeIssues=none
+            else:
+                if not isinstance(value, Yosys.Settings):
+                    value = Yosys.Settings(**value)
+                value.fpga = fpga
+                value.clocks = clocks
+            return value
+
+    def init(self) -> None:
+        assert isinstance(self.settings, self.Settings)
+        ss = self.settings
+        assert ss.yosys is not None
+        self.add_dependency(
+            Yosys,
+            ss.yosys,
+        )
+
+    def run(self) -> None:
+        assert isinstance(self.settings, self.Settings)
+        ss = self.settings
+        yosys_flow = self.completed_dependencies[0]
+        assert isinstance(yosys_flow, Yosys)
+        assert isinstance(yosys_flow.settings, Yosys.Settings)
+        assert yosys_flow.settings.netlist_json
+        netlist_json = yosys_flow.run_path / yosys_flow.settings.netlist_json
+        fpga_family = ss.fpga.family if ss.fpga.family else "generic"
+        assert fpga_family in {
+            "generic",
+            "ecp5",
+            "ice40",
+            "nexus",
+            "gowin",
+            "fpga-interchange",
+            "xilinx",
+        }, "unsupported fpga family"
+        next_pnr = Tool(f"nextpnr-{fpga_family}")
+
+        if not netlist_json.exists():
+            raise FlowFatalError(f"netlist json file {netlist_json} does not exist!")
+
+        lpf = ss.lpf_cfg
+        board_data = get_board_data(ss.board)
+        if not lpf and board_data and "lpf" in board_data:
+            uri = board_data["lpf"]
+            r = urlparse(uri)
+            if r.scheme and r.netloc:
+                try:
+                    lpf, _ = urlretrieve(uri)
+                except HTTPError as e:
+                    log.critical(
+                        "Unable to retrive file from %s (HTTP Error %d)",
+                        uri,
+                        e.code,
+                    )
+                    raise FlowFatalError("Unable to retreive LPF file") from None
+            else:
+                if "name" in board_data:
+                    board_name = board_data["name"]
+                else:
+                    board_name = ss.board
+                lpf = get_board_file_path(f"{board_name}.lpf")
+
+        args = setting_flag(netlist_json, name="json")
+        args += setting_flag(ss.clock_period and (1000 / ss.clock_period), name="freq")
+        args += setting_flag(lpf)
+        args += setting_flag(self.design.rtl.top)
+        args += setting_flag(ss.seed)
+        args += setting_flag(ss.fpga.speed)
+        if ss.fpga.capacity:
+            device_type = ss.fpga.type
+            if device_type and device_type.lower() != "u":
+                args.append(f"--{device_type}-{ss.fpga.capacity}")
+            else:
+                args.append(f"--{ss.fpga.capacity}")
+        args += setting_flag(ss.out_of_context)
+        args += setting_flag(ss.lpf_allow_unconstrained)
+        args += setting_flag(ss.debug)
+        args += setting_flag(ss.verbose)
+        args += setting_flag(ss.quiet)
+        args += setting_flag(ss.randomize_seed)
+        args += setting_flag(ss.timing_allow_fail)
+        args += setting_flag(ss.ignore_loops)
+        args += setting_flag(ss.py_script, name="run")
+        package = ss.fpga.package
+        if ss.fpga.vendor and ss.fpga.vendor.lower() == "lattice":
+            if package:
+                package = package.upper()
+                if package == "BG":
+                    package = "CABGA"
+                elif package == "MG":
+                    package = "CSFBGA"
+                assert ss.fpga.pins
+                package += str(ss.fpga.pins)
+        args += setting_flag(package)
+        args += setting_flag(lpf)
+        args += setting_flag(ss.textcfg)
+        args += setting_flag(ss.write)
+        args += setting_flag(ss.nthreads, name="threads")
+        args += setting_flag(ss.sdf)
+        args += setting_flag(ss.log)
+        args += setting_flag(ss.report)
+        args += setting_flag(ss.placed_svg)
+        args += setting_flag(ss.routed_svg)
+        args += setting_flag(ss.detailed_timing_report)
+        args += setting_flag(ss.parallel_refine)
+        if ss.extra_args:
+            args += ss.extra_args
+        next_pnr.run(*args)
diff --git a/src/xeda/flows/openfpgaloader.py b/src/xeda/flows/openfpgaloader.py
new file mode 100644
index 00000000..ee0f151e
--- /dev/null
+++ b/src/xeda/flows/openfpgaloader.py
@@ -0,0 +1,82 @@
+import logging
+from pathlib import Path
+from typing import Optional
+
+from ..board import WithFpgaBoardSettings, get_board_data
+from ..design import Design
+from ..dataclass import validator
+from ..tool import Tool
+from .flow import FpgaSynthFlow
+from .nextpnr import Nextpnr
+
+__all__ = ["Openfpgaloader"]
+
+log = logging.getLogger(__name__)
+
+
+class Openfpgaloader(FpgaSynthFlow):
+    ofpga_loader = Tool("openFPGALoader")
+
+    class Settings(WithFpgaBoardSettings):
+        clock_period: float
+        reset: bool = False
+        cable: Optional[str] = None
+        nextpnr: Optional[Nextpnr.Settings] = None
+
+        @validator("nextpnr", always=True, pre=True)
+        def _validate_nextpnr(cls, value, values):
+            clocks = values.get("clocks")
+            fpga = values.get("fpga")
+            board = values.get("board")
+            if value is None:
+                value = {}
+            if isinstance(value, Nextpnr.Settings):
+                value = value.dict()
+            assert isinstance(value, (dict)), f"not a dict: {value}"
+            value["fpga"] = fpga
+            value["board"] = board
+            value["clocks"] = clocks
+            return Nextpnr.Settings(**value)
+
+    def __init__(self, flow_settings: Settings, design: Design, run_path: Path):
+        super().__init__(flow_settings, design, run_path)
+        self.packer: Optional[Tool] = None
+
+    def init(self) -> None:
+        assert isinstance(self.settings, self.Settings)
+        ss = self.settings
+        assert ss.nextpnr is not None
+        self.add_dependency(Nextpnr, ss.nextpnr)
+        if ss.fpga.family == "ecp5":  # FIXME from fpga/board
+            self.packer = Tool("ecppack")
+
+    def run(self) -> None:
+        assert isinstance(self.settings, self.Settings)
+        ss = self.settings
+        board_id = ss.board
+        board_name = None
+        if board_id:
+            board_data = get_board_data(board_id)
+            if board_data:
+                board_name = board_data.get("name")
+        next_pnr = self.completed_dependencies[0]
+        assert isinstance(next_pnr, Nextpnr)
+        assert isinstance(next_pnr.settings, Nextpnr.Settings)
+        assert next_pnr.settings.textcfg
+        text_cfg = next_pnr.run_path / next_pnr.settings.textcfg
+        assert text_cfg.exists(), f"Can't find {text_cfg} generated by Nextpnr!"
+        bitstream = f"{board_name}.bit" if board_name else "bitstream.bit"
+        if self.packer:
+            self.packer.run(text_cfg, bitstream)
+        args = ["--bitstream", bitstream]
+        if ss.cable:
+            args.extend(["--cable", ss.cable])
+        elif board_name:
+            args.extend(["--board", board_name])
+        if ss.fpga.part:
+            args.extend(["--fpga-part", ss.fpga.part])
+        if ss.reset:
+            args.append("--reset")
+        if ss.verbose:
+            args.append("--verbose")
+        self.ofpga_loader.run(*args)
diff --git a/src/xeda/flows/quartus/__init__.py b/src/xeda/flows/quartus/__init__.py
new file mode 100644
index 00000000..3b5606ee
--- /dev/null
+++ b/src/xeda/flows/quartus/__init__.py
@@ -0,0 +1,411 @@
+"""Intel Quartus flows"""
+import csv
+import logging
+from pathlib import Path
+from typing import Any, Callable, Dict, Literal, Optional, Set
+
+from ...dataclass import Field
+from ...tool import Docker, Tool
+from ...types import PathLike
+from ...utils import try_convert
+from ..flow import FpgaSynthFlow
+
+log = logging.getLogger(__name__)
+
+
+def identity(x: Any) -> Any:
+    return x
+
+
+def parse_csv(
+    path: PathLike,
+    id_field: Optional[str],
+    field_parser: Callable[[str], Any] = identity,
+    id_parser: Callable[[str], Any] = identity,
+    interesting_fields: Optional[Set[str]] = None,
+) -> Dict[str, Any]:
+    """Parse TCL-generated CSV file"""
+    data: Dict[str, Any] = {}
+    with open(path, newline="") as csvfile:
+        if id_field:
+            # with header, and some rows of data indexed by id_field
+            reader = csv.DictReader(csvfile)
+            for row in reader:
+                if interesting_fields is None:
+                    interesting_fields = set(row)
+                row_id = row.get(id_field)
+                if row_id:
+                    data[id_parser(row_id)] = {
+                        k: field_parser(row[k]) for k in interesting_fields if k in row
+                    }
+                else:
+                    log.critical("ID field %s not found in row (%s)", id_field, row)
+        else:
+            # no header, key/value pairs on each line
+            for lrow in csv.reader(csvfile):
+                if len(lrow) >= 2:
+                    data[id_parser(lrow[0])] = field_parser(",".join(lrow[1:]))
+                else:
+                    log.critical("Line is not in key,value format: %s", lrow)
+    return data
+
+
+def try_num(s: str):
+    s = s.strip()
+    try:
+        return int(s)
+    except ValueError:
+        try:
+            return float(s)
+        except ValueError:
+            return s
+
+
+def try_float(s: str):
+    s = s.strip()
+    try:
+        return float(s)
+    except ValueError:
+        return s
+
+
+class Quartus(FpgaSynthFlow):
+    """FPGA synthesis using Intel Quartus"""
+
+    quartus_sh = Tool(
+        executable="quartus_sh",
+        docker=Docker(
+            command=["quartus_wrapper", "quartus_sh"],
+            image="chriz2600/quartus-lite",
+            tag="20.1.0",
+            platform="linux/amd64",
+        ),  # type: ignore
+    )
+
+    class Settings(FpgaSynthFlow.Settings):
+        # part number (fpga.part) formats are quite complicated.
+        # See: https://www.intel.com/content/dam/www/central-libraries/us/en/documents/product-catalog.pdf
+        seed: Optional[int] = Field(None, description="Seed")
+        optimization_mode: Literal[
+            "BALANCED",
+            "HIGH PERFORMANCE EFFORT",
+            "AGGRESSIVE PERFORMANCE",
+            "High Performance with Maximum Placement Effort",
+            "Superior Performance",
+            "Superior Performance with Maximum Placement Effort",
+            "Aggressive Area",
+            "High Placement Routability Effort",
+            "High Packing Routability Effort",
+            "Optimize Netlist for Routability",
+            "High Power Effort",
+        ] = Field(
+            "HIGH PERFORMANCE EFFORT",
+            description="""
+            see https://www.intel.com/content/www/us/en/programmable/documentation/zpr1513988353912.html
+            https://www.intel.com/content/www/us/en/programmable/quartushelp/current/index.htm
+        """,
+        )
+        remove_redundant_logic: bool = True
+        auto_resource_sharing: bool = True
+        retiming: bool = True
+        register_duplication: bool = True
+        packed_registers: bool = False
+        gated_clock_conversion: bool = False
+        dsp_recognition: bool = True
+        ram_recognition: bool = True
+        rom_recognition: bool = True
+        synthesis_effort: Literal["auto", "fast"] = "auto"
+        fitter_effort: Literal["STANDARD FIT", "AUTO FIT", "FAST_FIT"] = "AUTO FIT"
+        optimization_technique: Literal["AREA", "SPEED", "BALANCED"] = "SPEED"
+        placement_effort_multiplier: float = Field(
+            2.0,
+            description="""
+        A logic option that controls how much time the Fitter spends in placement.
+        The default value is 1.0 and legal values must be greater than 0 and can be non-integer values.
+        Values between 0 and 1 can reduce fitting time, but also can reduce placement quality and design performance.
+        Values greater than 1 increase placement time and placement quality, but may reduce routing time for designs with routing congestion.
+        For example, a value of 4 increases fitting time by approximately 2 to 4 times, but may improve quality.""",
+        )
+        router_timing_optimization_level: Literal[
+            "Normal", "Maximum", "Minimum"
+        ] = "Maximum"
+        final_placement_optimization: Literal[
+            "ALWAYS", "AUTOMATICALLY", "NEVER"
+        ] = "ALWAYS"
+
+    def init(self) -> None:
+        self.reports_dir.mkdir(exist_ok=True)
+        self.reports = {
+            "summary": self.reports_dir / "Flow_Summary.csv",
+            "utilization": self.reports_dir
+            / "Fitter"
+            / "Resource_Section"
+            / "Fitter_Resource_Utilization_by_Entity.csv",
+            "timing_dir": self.reports_dir / "Timing_Analyzer",
+            "timing.multicorner_summary": self.reports_dir
+            / "Timing_Analyzer"
+            / "Multicorner_Timing_Analysis_Summary.csv",
+        }
+        # FIXME only a proof of concept. Tool instantiation/invocation needs to change!
+        if self.quartus_sh.docker:
+            self.quartus_sh.docker.enabled = self.settings.dockerized
+            # FIXME
+            for src in self.design.rtl.sources:
+                p = str(src.file.parent.resolve())
+                self.quartus_sh.docker.mounts[p] = p
+            if self.settings.dockerized and self.quartus_sh.docker.nproc:
+                self.settings.ncpus = min(
+                    self.settings.ncpus, self.quartus_sh.docker.nproc
+                )
+                self.settings.nthreads = min(
+                    self.settings.nthreads, self.quartus_sh.docker.nproc
+                )
+
+    def create_project(self, **kwargs: Any) -> None:
+        assert isinstance(self.settings, self.Settings)
+        ss = self.settings
+
+        project_settings = {
+            "SEED": ss.seed,
+            "OPTIMIZATION_MODE": ss.optimization_mode,
+            "REMOVE_REDUNDANT_LOGIC_CELLS": ss.remove_redundant_logic,
+            "AUTO_RESOURCE_SHARING": ss.auto_resource_sharing,
+            "ALLOW_REGISTER_RETIMING": ss.retiming,
+            "SYNTH_GATED_CLOCK_CONVERSION": ss.gated_clock_conversion,
+            # faster:
+            "FITTER_EFFORT": ss.fitter_effort,
+            # AREA, SPEED, BALANCED
+            "STRATIX_OPTIMIZATION_TECHNIQUE": ss.optimization_technique,
+            "CYCLONE_OPTIMIZATION_TECHNIQUE": ss.optimization_technique,
+            # see https://www.intel.com/content/www/us/en/programmable/documentation/rbb1513988527943.html
+            "PLACEMENT_EFFORT_MULTIPLIER": ss.placement_effort_multiplier,
+            "ROUTER_TIMING_OPTIMIZATION_LEVEL": ss.router_timing_optimization_level,
+            "FINAL_PLACEMENT_OPTIMIZATION": ss.final_placement_optimization,
+            # "PHYSICAL_SYNTHESIS_COMBO_LOGIC_FOR_AREA": True,
+            # ?
+            # "ADV_NETLIST_OPT_SYNTH_GATE_RETIME": True,
+            # ?
+            # "ADV_NETLIST_OPT_SYNTH_WYSIWYG_REMAP": True,
+            "AUTO_PACKED_REGISTERS_STRATIX": ss.packed_registers,
+            "AUTO_PACKED_REGISTERS_CYCLONE": ss.packed_registers,
+            "PHYSICAL_SYNTHESIS_COMBO_LOGIC": True,
+            "PHYSICAL_SYNTHESIS_REGISTER_DUPLICATION": ss.register_duplication,
+            "PHYSICAL_SYNTHESIS_REGISTER_RETIMING": ss.retiming,
+            # "PHYSICAL_SYNTHESIS_EFFORT": "EXTRA",
+            # NORMAL, OFF, EXTRA_EFFORT
+            # "OPTIMIZE_POWER_DURING_SYNTHESIS": "NORMAL",
+            # SYNTH_CRITICAL_CLOCK: ON, OFF : Speed Optimization Technique for Clock Domains}
+            "AUTO_DSP_RECOGNITION": ss.dsp_recognition,
+            "AUTO_RAM_RECOGNITION": ss.ram_recognition,
+            "AUTO_ROM_RECOGNITION": ss.rom_recognition,
+            "FLOW_ENABLE_POWER_ANALYZER": True,
+        }
+
+        clock_sdc_path = self.copy_from_template("clock.sdc")
+        script_path = self.copy_from_template(
+            "create_project.tcl",
+            sdc_files=[clock_sdc_path],
+            project_settings=project_settings,
+            **kwargs,
+        )
+        self.quartus_sh.run("-t", script_path)
+
+        # self.run_process('quartus_sh',
+        #                  ['--dse', '-project', self.settings.design['name'], '-nogui', '-concurrent-compiles', '8', '-exploration-space',
+        #                   "Extra Effort Space", '-optimization-goal', "Optimize for Speed", '-report-all-resource-usage', '-ignore-failed-base'],
+        #                  stdout_logfile='dse_stdout.log'
+        #                  )
+
+    # def __init__(self, settings, args, logger):
+    #     # def supported_quartus_generic(k, v, sim):
+    #     #     if sim:
+    #     #         return True
+    #     #     if isinstance(v, int):
+    #     #         return True
+    #     #     if isinstance(v, bool):
+    #     #         return True
+    #     #     v = str(v)
+    #     #     return (v.isnumeric() or (v.strip().lower() in {'true', 'false'}))
+
+    #     # def quartus_gen_convert(k, x, sim):
+    #     #     if sim:
+    #     #         if isinstance(x, dict) and "file" in x:
+    #     #             p = x["file"]
+    #     #             assert isinstance(p, str), "value of `file` should be a relative or absolute path string"
+    #     #             x = self.conv_to_relative_path(p.strip())
+    #     #             self.logger.info(f'Converting generic `{k}` marked as `file`: {p} -> {x}')
+    #     #     xl = str(x).strip().lower()
+    #     #     if xl == 'false':
+    #     #         return "1\\'b0"
+    #     #     if xl == 'true':
+    #     #         return "1\\'b1"
+    #     #     return x
+
+    #     # def quartus_generics(kvdict, sim):
+    #     #     return ' '.join([f"-generic {k}={quartus_gen_convert(k, v, sim)}" for k, v in kvdict.items() if supported_quartus_generic(k, v, sim)])
+
+    #     super().__init__(settings, args, logger)
+
+    #     # self.settings.flow['generics_options'] = quartus_generics(self.settings.design["generics"], sim=False)
+    #     # self.settings.flow['tb_generics_options'] = quartus_generics(self.settings.design["tb_generics"], sim=True)
+
+    # self.run_process('quartus_eda', [prj_name, '--simulation', '--functional', '--tool=modelsim_oem', '--format=verilog'],
+    #                         stdout_logfile='eda_1_stdout.log'
+    #                         )
+
+    def run(self) -> None:
+        self.create_project()
+        script_path = self.copy_from_template(
+            "compile.tcl", reports_dir=self.reports_dir
+        )
+        self.quartus_sh.run("-t", script_path)
+
+    def parse_reports(self) -> bool:
+        assert isinstance(self.settings, self.Settings)
+        failed = False
+        reports = self.reports
+        resources = parse_csv(reports["summary"], id_field=None)
+        self.results.update(resources)
+        utilization_report = Path(reports["utilization"])
+        assert utilization_report.exists()
+        resources = parse_csv(
+            utilization_report,
+            id_field="Compilation Hierarchy Node",
+            field_parser=lambda s: try_num(s.split()[0]),
+            id_parser=lambda s: s.strip().lstrip("|"),
+            # interesting_fields={
+            #     "Logic Cells",
+            #     "LUT-Only LCs",
+            #     "Register-Only LCs",
+            #     "LUT/Register LCs",
+            #     "Dedicated Logic Registers",
+            #     "ALMs needed [=A-B+C]",
+            #     "Combinational ALUTs",
+            #     "ALMs used for memory",
+            #     "Memory Bits",
+            #     "M10Ks",
+            #     "M9Ks",
+            #     "DSP Elements",
+            #     "DSP Blocks",
+            #     "DSP 9x9",
+            #     "DSP 18x18"
+            #     "Block Memory Bits",
+            #     "Pins",
+            #     "I/O Registers",
+            # },
+        )
+
+        top_resources = resources.get(self.design.rtl.top)
+        if top_resources:
+            top_resources.setdefault(0)
+            lut_only = top_resources.get("LUT-Only LCs", 0)
+            lut_reg = top_resources.get("LUT/Register LCs", 0)
+            top_resources["lut"] = lut_only + lut_reg
+            reg_only = top_resources.get("Register-Only LCs", 0)
+            top_resources["ff"] = reg_only + lut_reg
+            self.results.update(top_resources)
+
+        # TODO reference for why this timing report is chosen
+
+        mc_report = reports["timing.multicorner_summary"]
+
+        slacks = parse_csv(
+            mc_report,
+            id_field="Clock",
+            field_parser=try_float,
+            id_parser=lambda s: s.strip(),
+            interesting_fields={"Setup", "Hold"},
+        )
+        print("slacks:", slacks)
+        worst_slacks = slacks.get("Worst-case Slack")
+        print("worst_slacks:", worst_slacks)
+        if worst_slacks:
+            wns = worst_slacks.get("Setup")
+            print("wns:", wns)
+            whs = worst_slacks.get("Hold")
+            if wns is not None:
+                wns = try_convert(wns)
+                self.results["wns"] = wns
+                if isinstance(wns, (int, float)):
+                    failed |= wns < 0
+            if whs is not None:
+                whs = try_convert(whs)
+                self.results["whs"] = whs
+                if isinstance(whs, (int, float)):
+                    failed |= whs < 0
+
+        timing_reports_folder = Path(reports["timing_dir"])
+        max_fmax = 0.0
+        for fmax_report in timing_reports_folder.glob(
+            "Slow_*_Model/Slow_*_Model_Fmax_Summary.csv"
+        ):
+            log.info("Parsing timing report: %s", fmax_report)
+            fmax = parse_csv(
+                fmax_report,
+                id_field="Clock Name",
+                field_parser=lambda s: s.strip().split(),
+                id_parser=lambda s: s.strip(),
+                interesting_fields={"Fmax"},
+            )
+            conditions = (
+                fmax_report.parent.name.lstrip("Slow_").rstrip("_Model").split("_")
+            )
+            for clock in self.settings.clocks.keys():
+                flst = fmax.get(clock, {}).get("Fmax")
+                assert flst and len(flst) == 2
+                fmhz = float(flst[0])
+                if flst[1] == "GHz":
+                    fmhz *= 1000.0
+                else:
+                    assert flst[1] == "MHz"
+                if fmhz > max_fmax:
+                    max_fmax = fmhz
+                self.results[f'{clock} Fmax@{":".join(conditions)} (MHz)'] = fmhz
+        if max_fmax:
+            self.results["Fmax"] = max_fmax
+
+        return not failed
+
+
+# class QuartusDse(QuartusSynth, DseFlow):
+#     def run(self):
+#         self.create_project()
+#         # 'explore': Exploration flow to use, if not specified in --config
+#         #   configuration file. Valid flows: timing_aggressive,
+#         #   all_optimization_modes, timing_high_effort, seed,
+#         #   area_aggressive, power_high_effort, power_aggressive
+#         # 'compile_flow':  'full_compile', 'fit_sta' and 'fit_sta_asm'.
+#         # 'timeout': Limit the amount of time a compute node is allowed to run. Format: hh:mm:ss
+#         if 'dse' not in self.settings.flow:
+#             self.fatal('`flows.quartus.dse` settings are missing!')
+
+#         dse = self.settings.flow['dse']
+#         if 'nproc' not in dse or not dse['nproc']:
+#             dse['nproc'] = self.nthreads
+
+#         script_path = self.copy_from_template(f'settings.dse',
+#                                               dse=dse
+#                                               )
+#         self.run_process('quartus_dse',
+#                          ['--use-dse-file', script_path, self.settings.design['name']],
+#                          stdout_logfile='dse_stdout.log',
+#                          initial_step="Running Quartus DSE",
+#                          )
+
+# DES:
+
+# Available exploration spaces for this family are:
+# "Seed Sweep"
+# "Extra Effort Space"
+# "Extra Effort Space for Quartus Prime Integrated Synthesis Projects"
+# "Area Optimization Space"
+# "Signature: Placement Effort Multiplier"
+# "Custom Space"
+
+# Valid optimization-goal options are:
+# "Optimize for Speed"
+# "Optimize for Area"
+# "Optimize for Power"
+# "Optimize for Negative Slack and Failing Paths"
+# "Optimize for Average Period"
+# "Optimize for Quality of Fit"
diff --git a/src/xeda/flows/quartus/templates/clock.sdc b/src/xeda/flows/quartus/templates/clock.sdc
new file mode 100644
index 00000000..ca69c923
--- /dev/null
+++ b/src/xeda/flows/quartus/templates/clock.sdc
@@ -0,0 +1,9 @@
+{% for clock_name,clock in settings.clocks.items() -%}
+{% if clock.port -%}
+# create_clock -period {{ "%.3f"|format(clock.period) }} -name {{clock_name}} [get_ports {{clock.port}}]
+create_clock -period {{clock.period|round(3,'floor')}} -name {{clock_name}} [get_ports {{clock.port}}]
+{% endif -%}
+{% endfor -%}
+
+derive_pll_clocks
+derive_clock_uncertainty
\ No newline at end of file
diff --git a/xeda/flows/quartus/templates/compile.tcl b/src/xeda/flows/quartus/templates/compile.tcl
similarity index 73%
rename from xeda/flows/quartus/templates/compile.tcl
rename to src/xeda/flows/quartus/templates/compile.tcl
index 42c0484e..95cb8a0d 100644
--- a/xeda/flows/quartus/templates/compile.tcl
+++ b/src/xeda/flows/quartus/templates/compile.tcl
@@ -1,35 +1,49 @@
-set design_name           {{design.name}}
+set project_name          {{design.name}}
 set top                   {{design.rtl.top}}
-set debug                 {{debug}}
 
 package require ::quartus::project
 package require ::quartus::flow
 
-project_open ${design_name}
+project_open ${project_name}
 
 load_package flow
 
-puts "\n===========================( Running flow )==========================="
+puts "\n===========================( Running compile flow )==========================="
 # runs: quartus_map, quartus_fit, quartus_asm, and quartus_sta
 if {[catch {execute_flow -compile} result]} {
     puts "ERROR: Compilation failed. Result: $result. See report files.\n"
-    exit 1
+    qexit -error
 }
 
+puts "clocks: [get_clocks]"
 
 # TODO set up: verilog include-dirs, VHDL generics, verilog params,
 
-
 load_package report
 load_report
 
-set panel_names [get_report_panel_names]
 
-puts "panel_names=${panel_names}"
+set panel "Timing Analyzer||Setup Summary"
+set panel_id [get_report_panel_id $panel]
+
+# negative if panel not found
+if {$panel_id > 0} {   
+    set setup_slack [get_report_panel_data -col_name Slack -row 1 -id $panel_id]
+    puts ""
+    puts "-----------------------------------------------------"
+    puts "Setup slack: $setup_slack"
+    puts "-----------------------------------------------------"
+    puts ""
+    if {$setup_slack<0} {
+        puts "\[ERROR\] Timing not met!"
+        # qexit -error
+    }
+}
+
 
-set reports_dir {{reports_dir}}
+set panel_names [get_report_panel_names]
 
-file mkdir $reports_dir
+puts "panel_names=${panel_names}"
 
 foreach panel_name $panel_names {
 
@@ -45,7 +59,7 @@ foreach panel_name $panel_names {
     set csv_file [regsub -all {_*'+_*} $csv_file {} ]
     set csv_file [regsub -all {_*"+_*} $csv_file {} ]
 
-    set csv_file $reports_dir/$csv_file.csv
+    set csv_file {{reports_dir}}/$csv_file.csv
 
     set csv_file_dir [file dirname ${csv_file}]
 
diff --git a/src/xeda/flows/quartus/templates/create_project.tcl b/src/xeda/flows/quartus/templates/create_project.tcl
new file mode 100644
index 00000000..8541672f
--- /dev/null
+++ b/src/xeda/flows/quartus/templates/create_project.tcl
@@ -0,0 +1,52 @@
+set design_name           {{design.name}}
+set top                   {{design.rtl.top}}
+set fpga_part             {{settings.fpga.part}}
+{%- if settings.debug %}
+foreach key [array names quartus] {
+    puts "${key}=$quartus($key)"
+}
+{%- endif %}
+
+package require ::quartus::project
+
+puts "\n===========================( Setting up project and settings )==========================="
+project_new ${design_name} -overwrite
+
+set_global_assignment -name NUM_PARALLEL_PROCESSORS {{settings.ncpus}}
+
+puts "supported FPGA families: [get_family_list]"
+
+set fpga_part_report [report_part_info $fpga_part]
+puts $fpga_part_report
+
+{%- if settings.fpga.family %}
+set_global_assignment -name FAMILY "{{settings.fpga.family}}"
+{%- endif %}
+# Use get_part_list to get a list of supported part numbers
+set_global_assignment -name DEVICE $fpga_part
+
+set_global_assignment -name TOP_LEVEL_ENTITY ${top}
+
+{%- if design.language.vhdl.standard == "08" %}
+set_global_assignment -name VHDL_INPUT_VERSION VHDL_2008
+{%- endif %}
+
+{%- for src in design.rtl.sources %}
+set_global_assignment -name {% if src.type == "verilog" and src.variant == "systemverilog" -%} SYSTEMVERILOG {%- else -%} {{src.type|upper}} {%- endif -%}_FILE {{src.file}}
+{%- endfor %}
+
+{%- for k,v in design.rtl.parameters.items() %}
+set_parameter -name {{k}} {%if v is boolean -%} {{"true" if v else "false"}} {% elif v is string -%} "{{v}}" {%else-%} {{v}} {%endif%}
+{%- endfor %}
+
+{%- for sdc_file in sdc_files %}
+set_global_assignment -name SDC_FILE {{sdc_file}}
+{%- endfor %}
+
+{%- for k,v in project_settings.items() %}
+{%- if v is not none %}
+set_global_assignment -name {{k}} {%if v is boolean -%} {{"ON" if v else "OFF"}}  {% elif v is number -%} {{v}} {%- else -%} "{{v}}" {%- endif %}
+{%- endif %}
+{%- endfor %}
+
+project_close
\ No newline at end of file
diff --git a/xeda/flows/quartus/templates/settings.dse b/src/xeda/flows/quartus/templates/settings.dse
similarity index 100%
rename from xeda/flows/quartus/templates/settings.dse
rename to src/xeda/flows/quartus/templates/settings.dse
diff --git a/src/xeda/flows/vcs.py b/src/xeda/flows/vcs.py
new file mode 100644
index 00000000..108429fb
--- /dev/null
+++ b/src/xeda/flows/vcs.py
@@ -0,0 +1,79 @@
+from copy import deepcopy
+import logging
+from typing import List, Optional
+from .flow import SimFlow
+from ..tool import Tool
+
+log = logging.getLogger(__name__)
+
+
+class Vcs(SimFlow):
+    """Synopsys VCS simulator"""
+
+    dc_shell = Tool(executable="dc_shell-xg-t")  # type: ignore
+    vlogan = Tool("vlogan")
+    vhdlan = Tool("vhdlan")
+    vcs = Tool("vcs")
+
+    class Settings(SimFlow.Settings):
+        simv: str = "simv"
+        work_dir: Optional[str] = "work"
+        timescale: Optional[str] = "1ns/1ps"
+        vlogan_flags: List[str] = ["-full64", "-nc", "+warn=all"]  # TODO
+        vhdlan_flags: List[str] = ["-full64", "-nc"]  # "-cycle", "-event" "+warn=all"?
+        vcs_flags: List[str] = ["-full64", "-nc"]
+        vcs_log_file: Optional[str] = "vcs.log"
+
+    def run(self):
+        assert isinstance(self.settings, self.Settings)
+        ss = self.settings
+        with open("synopsys_sim.setup", "w", encoding="utf-8") as f:
+            f.writelines(
+                [
+                    "WORK  > default",
+                    f"default : {ss.work_dir}",
+                ]
+            )
+        vlogan_args = deepcopy(ss.vlogan_flags)
+        vhdlan_args = deepcopy(ss.vhdlan_flags)
+        if ss.work_dir:
+            vlogan_args.extend(["-work", ss.work_dir])
+            # vhdlan_args.extend(["-work", ss.work_dir]) # ???
+        if ss.timescale:
+            vlogan_args.append(f"-timescale={ss.timescale}")
+            vhdlan_args.append(f"-timescale={ss.timescale}")
+        # FIXME
+        incdirs: List[str] = []
+        for d in incdirs:
+            vlogan_args.append(f"+incdir+{d}")
+        vhdlan_args.append("-cycle")  # ???
+        vhdlan_args.extend("-event")  # ???
+        if self.design.language.vhdl.standard == "2008":
+            vhdlan_args.extend("-vhdl08")
+
+        # FIXME
+        vlogan_args += ["-sverilog", "+v2k"]
+        top = self.design.tb.top[0]
+        vcs_args = [top, *ss.vcs_flags]
+        vcs_args += [
+            f"-j{ss.nthreads}",
+            "-notice",
+            "+lint=all,noVCDE,noTFIPC,noIWU,noOUDPE",
+        ]
+
+        if ss.vcs_log_file:
+            vcs_args += ["-l", ss.vcs_log_file]
+
+        log.info("analyzing Verilog files")
+        self.vlogan.run(*vlogan_args)
+        log.info("analyzing VHDL files")
+        self.vhdlan.run(*vhdlan_args)
+        log.info("Running vcs")
+        self.vcs.run(*vcs_args)
+
+        simv = Tool("./simv")  # ???
+        simv_args = ["-nc", "-lca"]
+        for k, v in self.design.tb.parameters.items():
+            simv_args.append(f"+define+{k}={v}")
+        simv_args.extend(["-l", top])
+        simv.run(*simv_args)
diff --git a/src/xeda/flows/vivado/__init__.py b/src/xeda/flows/vivado/__init__.py
new file mode 100644
index 00000000..0e3dce96
--- /dev/null
+++ b/src/xeda/flows/vivado/__init__.py
@@ -0,0 +1,117 @@
+import logging
+from abc import ABCMeta
+from functools import reduce
+from html import unescape
+from pathlib import Path
+from typing import Any, Dict, Optional
+from xml.etree import ElementTree
+
+
+from ...dataclass import Field
+from ...design import Design
+from ...tool import Docker, Tool
+from ..flow import Flow
+
+log = logging.getLogger(__name__)
+
+
+def vivado_generics(kvdict, sim=False):
+    def supported_vivado_generic(v, sim):
+        if sim:
+            return True
+        if isinstance(v, int):
+            return True
+        if isinstance(v, bool):
+            return True
+        v = str(v)
+        return v.isnumeric() or (v.strip().lower() in {"true", "false"})
+
+    def vivado_gen_convert(k, x, sim):
+        if sim:
+            return x
+        xl = str(x).strip().lower()
+        if xl == "false":
+            return "1\\'b0"
+        if xl == "true":
+            return "1\\'b1"
+        return x
+
+    return " ".join(
+        [
+            f"-generic{'_top' if sim else ''} {k}={vivado_gen_convert(k, v, sim)}"
+            for k, v in kvdict.items()
+            if supported_vivado_generic(v, sim)
+        ]
+    )
+
+
+class Vivado(Flow, metaclass=ABCMeta):
+    """Xilinx (AMD) Vivado FPGA synthesis and simulation flows"""
+
+    class Settings(Flow.Settings):
+        tcl_shell: bool = Field(
+            False,
+            description="Drop to interactive TCL shell after Vivado finishes running a flow script",
+        )
+
+    def __init__(self, settings: Settings, design: Design, run_path: Path):
+        super().__init__(settings, design, run_path)
+        default_args = [
+            "-nojournal",
+            "-mode",
+            "tcl" if settings.tcl_shell else "batch",
+        ]
+        if not self.settings.debug:
+            default_args.append("-notrace")
+        self.vivado = Tool(
+            "vivado",
+            default_args=default_args,
+            docker=Docker(
+                image="pwang7/vivado_ubuntu",
+                tag="standard-2021.2",
+                enabled=self.settings.dockerized,
+            ),
+        )
+        self.add_template_filter("vivado_generics", vivado_generics)
+
+    @staticmethod
+    def parse_xml_report(report_xml) -> Optional[Dict[str, Any]]:
+        try:
+            tree = ElementTree.parse(report_xml)
+        except ElementTree.ParseError as e:
+            log.critical("Parsing %s failed: %s", report_xml, e.msg)
+            return None
+        data = {}
+        for section in tree.findall("./section"):
+            section_title = section.get("title", "<section>")
+            for table in section.findall("./table"):
+                table_data = {}
+                header = [
+                    unescape(col.attrib["contents"]).strip()
+                    for col in table.findall("./tablerow/tableheader")
+                ]
+                for tablerow in table.findall("./tablerow"):
+                    cells = [
+                        unescape(cell.attrib["contents"]).strip()
+                        for cell in tablerow.findall("./tablecell")
+                    ]
+                    if cells:
+                        # choose 0th element as "index data" (distinct key)
+                        cell_data = {h: c for h, c in zip(header[1:], cells[1:]) if c}
+                        if cell_data:
+                            table_data[cells[0]] = cell_data
+                if table_data:
+                    table_title = table.get("title")
+                    title = (
+                        section_title + ":" + table_title
+                        if table_title
+                        else section_title
+                    )
+                    data[title] = table_data
+        return data
+
+    @staticmethod
+    def get_from_path(dct: dict, path):
+        if isinstance(path, str):
+            path = path.split(".")
+        return reduce(dict.__getitem__, path, dct)
diff --git a/xeda/plugins/__init__.py b/src/xeda/flows/vivado/templates/__init__.py
similarity index 100%
rename from xeda/plugins/__init__.py
rename to src/xeda/flows/vivado/templates/__init__.py
diff --git a/src/xeda/flows/vivado/templates/clock.xdc b/src/xeda/flows/vivado/templates/clock.xdc
new file mode 100644
index 00000000..e19e1d05
--- /dev/null
+++ b/src/xeda/flows/vivado/templates/clock.xdc
@@ -0,0 +1,10 @@
+{% for clock_name, clock in settings.clocks.items() -%}
+{% if clock.port -%}
+create_clock -period {{clock.period|round(3,'floor')}} -name {{clock_name}} [get_ports {{clock.port}}]
+{# FIXME: should be only I/O ports captured by or fed by clock_name #}
+{% if settings.input_delay  is not none -%} set_input_delay  -clock {{clock_name}} {{settings.input_delay}} [filter [all_inputs] {NAME != {{clock.port}} } ] {%- endif %}
+{% if settings.output_delay is not none -%} set_output_delay -clock {{clock_name}} {{settings.output_delay}} [all_outputs] {%- endif %}
+{% endif -%}
+{% endfor -%}
+
+set_units -power mW
\ No newline at end of file
diff --git a/xeda/flows/vivado/templates/util.tcl b/src/xeda/flows/vivado/templates/util.tcl
similarity index 93%
rename from xeda/flows/vivado/templates/util.tcl
rename to src/xeda/flows/vivado/templates/util.tcl
index b345dd16..e05e7d66 100644
--- a/xeda/flows/vivado/templates/util.tcl
+++ b/src/xeda/flows/vivado/templates/util.tcl
@@ -44,12 +44,10 @@ proc showWarningsAndErrors {} {
   if {$num_crit_warns > 0} {
     puts "\n===========================( *ENABLE ECHO* )==========================="
     puts "** Number of Critical Warnings:  $num_crit_warns"
-
-
-    if {$::fail_critical_warning} {
-      puts "Exiting due to $num_crit_warns critical warning(s)!"
-      exit 1
-    }
+    {% if settings.fail_critical_warning -%}
+    puts "Exiting due to $num_crit_warns critical warning(s)!"
+    exit 1
+    {%- endif %}
     puts "\n===========================( *DISABLE ECHO* )==========================="
   }
 
@@ -57,6 +55,6 @@ proc showWarningsAndErrors {} {
     puts "** Number of Warnings:           $num_warns"
   }
 
-  puts "\n\n"
+  puts "\n"
 }
 
diff --git a/xeda/flows/vivado/templates/vivado_synth.tcl b/src/xeda/flows/vivado/templates/vivado_alt_synth.tcl
similarity index 66%
rename from xeda/flows/vivado/templates/vivado_synth.tcl
rename to src/xeda/flows/vivado/templates/vivado_alt_synth.tcl
index 3c98242f..f123bd1f 100644
--- a/xeda/flows/vivado/templates/vivado_synth.tcl
+++ b/src/xeda/flows/vivado/templates/vivado_alt_synth.tcl
@@ -2,26 +2,21 @@
 # Generated by Xeda
 
 # These settings are set by Xeda
-set design_name           {{design.name}}
 set vhdl_std              {{design.language.vhdl.standard}}
-set nthreads              {{nthreads}}
+set nthreads              {{settings.nthreads}}
+set fail_critical_warning {{settings.fail_critical_warning}}
 
-set fail_critical_warning {{flow.fail_critical_warning}}
-set fail_timing           {{flow.fail_timing}}
-set bitstream             false
-
-set reports_dir           {{reports_dir}}
-set synth_output_dir      {{synth_output_dir}}
-set checkpoints_dir       {{checkpoints_dir}}
+set reports_dir           {{settings.reports_dir}}
+set settings.outputs_dir  {{settings.settings.outputs_dir}}
+set checkpoints_dir       {{settings.checkpoints_dir}}
+set fpga_part             {{settings.fpga.part}}
 
 {% include 'util.tcl' %}
 
-# TODO move all strategy-based decisions to Python side
-puts "Using \"{{flow.strategy}}\" synthesis strategy"
 
-set_param general.maxThreads ${nthreads}
+set_param general.maxThreads {{settings.nthreads}}
 
-file mkdir ${synth_output_dir}
+file mkdir ${settings.outputs_dir}
 file mkdir ${reports_dir}
 file mkdir [file join ${reports_dir} post_synth]
 file mkdir [file join ${reports_dir} post_place]
@@ -48,14 +43,14 @@ set parts [get_parts]
 
 puts "\n================================( Read Design Files and Constraints )================================"
 
-if {[lsearch -exact $parts {{flow.fpga_part}}] < 0} {
-    puts "ERROR: device {{flow.fpga_part}} is not supported!"
+if {[lsearch -exact $parts $fpga_part] < 0} {
+    puts "ERROR: device $fpga_part is not supported!"
     puts "Supported devices:"
     puts [join $parts " "]
     quit
 }
 
-puts "Targeting device: {{flow.fpga_part}}"
+puts "Targeting device: $fpga_part"
 
 # DO NOT use per file vhdl version as not supported universally (even though our data structures support it)
 set vhdl_std_opt [expr {$vhdl_std == "08" ?  "-vhdl2008": ""}];
@@ -74,7 +69,7 @@ if { [catch {eval read_verilog {{src.file}} } myError]} {
 }
 {%- endif %}
 {%- endif %}
-{% if src.type == 'vhdl' %}
+{% if src.type == 'vhdl' -%}
 puts "Reading VHDL file {{src.file}} ${vhdl_std_opt}"
 if { [catch {eval read_vhdl ${vhdl_std_opt} {{src.file}} } myError]} {
     errorExit $myError
@@ -84,37 +79,34 @@ if { [catch {eval read_vhdl ${vhdl_std_opt} {{src.file}} } myError]} {
 
 # TODO: Skip saving some artifects in case timing not met or synthesis failed for any reason
 
-{% for xdc_file in xdc_files %}
+{% for xdc_file in xdc_files -%}
 read_xdc {{xdc_file}}
 {% endfor %}
 
 puts "\n===========================( RTL Synthesize and Map )==========================="
-## eval synth_design -rtl -rtl_skip_ip -top {{design.rtl.top}} {{options.synth}} {{generics_options}}
-## write_verilog -force ${synth_output_dir}/synth_rtl.v
+eval synth_design -part $fpga_part -top {{design.rtl.top}} {{settings.synth.steps.synth|flatten_dict}} {{design.rtl.generics|vivado_generics}}
 
-eval synth_design -part {{flow.fpga_part}} -top {{design.rtl.top}} {{options.synth}} {{generics_options}}
-{% if flow.strategy == "Debug" %}
+{% if settings.synth.strategy == "Debug" -%}
 set_property KEEP_HIERARCHY true [get_cells -hier * ]
 set_property DONT_TOUCH true [get_cells -hier * ]
 {% endif %}
 showWarningsAndErrors
 
 
-{% if 'opt' in options and options.opt != None and flow.strategy != "Debug" and flow.strategy != "Runtime" %}
+{% if settings.synth.steps.opt != None -%}
 puts "\n==============================( Optimize Design )================================"
-eval opt_design {{options.opt}}
+eval opt_design {{settings.synth.steps.opt|flatten_dict}}
 {% endif %}
 
-
-puts "==== Synthesis and Mapping Steps Complemeted ====\n"
+{% if settings.write_checkpoint -%}
 write_checkpoint -force ${checkpoints_dir}/post_synth
+{% endif %}
 report_timing_summary -file ${reports_dir}/post_synth/timing_summary.rpt
 report_utilization -hierarchical -force -file ${reports_dir}/post_synth/hierarchical_utilization.rpt
 # reportCriticalPaths ${reports_dir}/post_synth/critpath_report.csv
 # report_methodology  -file ${reports_dir}/post_synth/methodology.rpt
 
-## TODO FIXME
-{% if flow.optimize_power and not flow.optimize_power_postplace %}
+{% if settings.optimize_power and not settings.optimize_power_postplace -%}
 puts "\n===============================( Post-synth Power Optimization )================================"
 # this is more effective than Post-placement Power Optimization but can hurt timing
 eval power_opt_design
@@ -123,52 +115,58 @@ showWarningsAndErrors
 {% endif %}
 
 puts "\n================================( Place Design )================================="
-eval place_design {{options.place}}
+eval place_design {{settings.impl.steps.place|flatten_dict}}
 showWarningsAndErrors
 
 
-{% if flow.optimize_power_postplace %}
+{% if settings.optimize_power_postplace %}
 puts "\n===============================( Post-placement Power Optimization )================================"
 eval power_opt_design
 report_power_opt -file ${reports_dir}/post_place/post_place_power_optimization.rpt
 showWarningsAndErrors
 {% endif %}
 
-{% if 'place_opt' in options and options.place_opt != None %}
+{% if settings.impl.steps.place_opt != None -%}
 puts "\n==============================( Post-place optimization )================================"
-eval opt_design {{options.place_opt}}
-{% if options.place_opt2 != None %}
+eval opt_design {{settings.impl.steps.place_opt|flatten_dict}}
+
+{% if settings.impl.steps.place_opt2 != None -%}
 puts "\n==============================( Post-place optimization 2)================================"
-eval opt_design {{options.place_opt2}}
+eval opt_design {{settings.impl.steps.place_opt2|flatten_dict}}
 {% endif %}
+
 {% endif %}
 
 
-{% if options.phys_opt != None and flow.strategy != "Debug" and flow.strategy != "Runtime" %}
+{% if settings.impl.steps.phys_opt != None -%}
 puts "\n========================( Post-place Physical Optimization )=========================="
-eval phys_opt_design {{options.phys_opt}}
+eval phys_opt_design {{settings.impl.steps.phys_opt|flatten_dict}}
+{% if settings.impl.steps.phys_opt2 != None -%}
 puts "\n========================( Post-place Physical Optimization 2 )=========================="
-{% if options.place_opt2 != None %}
-eval phys_opt_design {{options.phys_opt2}}
+eval phys_opt_design {{settings.impl.steps.phys_opt2|flatten_dict}}
 {% endif %}
 {% endif %}
 
+{% if settings.write_checkpoint -%}
 write_checkpoint -force ${checkpoints_dir}/post_place
 report_timing_summary -file ${reports_dir}/post_place/timing_summary.rpt
 report_utilization -hierarchical -force -file ${reports_dir}/post_place/hierarchical_utilization.rpt
+{% endif %}
 
 puts "\n================================( Route Design )================================="
-eval route_design {{options.route}}
+eval route_design {{settings.impl.steps.route|flatten_dict}}
 showWarningsAndErrors
 
-{% if options.phys_opt != None and flow.strategy != "Debug" and flow.strategy != "Runtime" %}
+{% if settings.impl.steps.phys_opt != None -%}
 puts "\n=========================( Post-Route Physical Optimization )=========================="
-phys_opt_design
+phys_opt_design {{settings.impl.steps.phys_opt|flatten_dict}}
 showWarningsAndErrors
 {% endif %}
 
+{% if settings.write_checkpoint -%}
 puts "\n=============================( Writing Checkpoint )=============================="
 write_checkpoint -force ${checkpoints_dir}/post_route
+{% endif %}
 
 puts "\n==============================( Writing Reports )================================"
 report_timing_summary -check_timing_verbose -no_header -report_unconstrained -path_type full -input_pins -max_paths 10 -delay_type min_max -file ${reports_dir}/post_route/timing_summary.rpt
@@ -187,28 +185,33 @@ report_methodology                 -file ${reports_dir}/post_route/methodology.r
 ## report_qor_suggestions -force      -file ${reports_dir}/post_route/qor_suggestions.rpt
 
 set timing_slack [get_property SLACK [get_timing_paths]]
-puts "Final timing slack: $timing_slack ns"
 
-if {$timing_slack < 0} {
-    puts "\n===========================( *ENABLE ECHO* )==========================="
-    puts "ERROR: Failed to meet timing by $timing_slack, see [file join ${reports_dir} post_route timing_summary.rpt] for details"
-    if {$fail_timing} {
+if {[string is double -strict $timing_slack]} {
+    puts "Final timing slack: $timing_slack ns"
+
+    if {[string is double -strict $timing_slack] && ($timing_slack < 0)} {
+        puts "ERROR: Failed to meet timing by $timing_slack, see [file join ${reports_dir} post_route timing_summary.rpt] for details"
+    {% if settings.fail_timing -%}
         exit 1
+    {% endif %}
     }
-    puts "\n===========================( *DISABLE ECHO* )==========================="
-} else {
-    puts "\n==========================( Writing Netlist and SDF )============================="
-    write_sdf -mode timesim -process_corner slow -force -file ${synth_output_dir}/impl_timesim.sdf
-    # should match sdf
-    write_verilog -mode timesim -sdf_anno false -force -file ${synth_output_dir}/impl_timesim.v
-##    write_verilog -mode timesim -sdf_anno false -include_xilinx_libs -write_all_overrides -force -file ${synth_output_dir}/impl_timesim_inlined.v
-##    write_verilog -mode funcsim -force ${synth_output_dir}/impl_funcsim_noxlib.v
-##    write_vhdl    -mode funcsim -include_xilinx_libs -write_all_overrides -force -file ${synth_output_dir}/impl_funcsim.vhd
-    write_xdc -no_fixed_only -force ${synth_output_dir}/impl.xdc
-
-    if {${bitstream}} {
-        puts "\n==============================( Writing Bitstream )==============================="
-        write_bitstream -force ${synth_output_dir}/bitstream.bit
-    }
-    showWarningsAndErrors
-}
+} 
+
+{%- if settings.write_netlist -%}
+puts "\n==========================( Writing Netlist and SDF )============================="
+write_sdf -mode timesim -process_corner slow -force -file ${settings.outputs_dir}/impl_timesim.sdf
+# should match sdf
+write_verilog -mode timesim -sdf_anno false -force -file ${settings.outputs_dir}/impl_timesim.v
+##    write_verilog -mode timesim -sdf_anno false -include_xilinx_libs -write_all_overrides -force -file ${settings.outputs_dir}/impl_timesim_inlined.v
+##    write_verilog -mode funcsim -force ${settings.outputs_dir}/impl_funcsim_noxlib.v
+##    write_vhdl    -mode funcsim -include_xilinx_libs -write_all_overrides -force -file ${settings.outputs_dir}/impl_funcsim.vhd
+write_xdc -no_fixed_only -force ${settings.outputs_dir}/impl.xdc
+{% endif %}
+
+{%- if settings.write_bitstream -%}
+puts "\n==============================( Writing Bitstream )==============================="
+write_bitstream -force ${settings.outputs_dir}/bitstream.bit
+{% endif %}
+
+showWarningsAndErrors
+puts "\n===========================( *DISABLE ECHO* )==========================="
diff --git a/xeda/flows/vivado/templates/vivado_power.tcl b/src/xeda/flows/vivado/templates/vivado_power.tcl
similarity index 50%
rename from xeda/flows/vivado/templates/vivado_power.tcl
rename to src/xeda/flows/vivado/templates/vivado_power.tcl
index 0d6ae2fe..4bfe7118 100644
--- a/xeda/flows/vivado/templates/vivado_power.tcl
+++ b/src/xeda/flows/vivado/templates/vivado_power.tcl
@@ -1,10 +1,7 @@
 puts "\n================================( Opening routed design from checkpoint )================================="
 open_checkpoint {{checkpoint}}
 
-{% for rc in run_configs %}
-puts "\n================================( Reporting power from {{rc.saif}} )================================="
+puts "\n================================( Reporting power from {{settings.saif}} )================================="
 reset_switching_activity -all
-eval read_saif -no_strip -strip_path {{tb_top}}/{{design.tb.uut}} -verbose {{rc.saif}}
-report_power -hier all -format xml -verbose -file {{rc.report}}
-
-{% endfor %}
\ No newline at end of file
+eval read_saif -no_strip -strip_path {{self.design.tb.top[0]}}/{{design.tb.uut}} -verbose {{settings.saif}}
+report_power -hier all -format xml -verbose -file {{settings.power_report_xml}}
diff --git a/src/xeda/flows/vivado/templates/vivado_report_helper.tcl b/src/xeda/flows/vivado/templates/vivado_report_helper.tcl
new file mode 100644
index 00000000..5004654b
--- /dev/null
+++ b/src/xeda/flows/vivado/templates/vivado_report_helper.tcl
@@ -0,0 +1,45 @@
+{% include 'util.tcl' %}
+showWarningsAndErrors
+
+puts "\n==========================( Writing Reports after $ACTIVE_STEP )============================"
+set reports_dir [file join ../../{{settings.reports_dir}} $ACTIVE_STEP]
+set outputs_dir [file join ../../{{settings.outputs_dir}} $ACTIVE_STEP]
+file mkdir ${reports_dir}
+file mkdir ${outputs_dir}
+
+report_route_status -file [file join ${reports_dir} route_status.rpt]
+report_timing_summary -check_timing_verbose -no_header -report_unconstrained -path_type full -input_pins -max_paths 10 -delay_type min_max -file [file join ${reports_dir} timing_summary.rpt]
+report_timing  -no_header -input_pins  -unique_pins -sort_by group -max_paths 100 -path_type full -delay_type min_max -file [file join ${reports_dir} timing.rpt]
+report_utilization -force -file [file join ${reports_dir} utilization.xml]              -format xml
+report_utilization -force -file [file join ${reports_dir} hierarchical_utilization.xml] -format xml  -hierarchical 
+
+if {$ACTIVE_STEP == "route_design"} {
+    set timing_slack [get_property SLACK [get_timing_paths]]
+    puts "Final timing slack: $timing_slack ns"
+
+    {% if settings.qor_suggestions -%}
+    report_qor_suggestions -file [file join ${reports_dir} qor_suggestions.rpt] 
+    # -max_strategies 5
+    # write_qor_suggestions -force qor_suggestions.rqs
+    {%- endif %}
+    if {$timing_slack < 0} {
+        puts "\n===========================( *ENABLE ECHO* )==========================="
+        puts "ERROR: Failed to meet timing by $timing_slack, see [file join ${reports_dir} post_route timing_summary.rpt] for details"
+        puts "\n===========================( *DISABLE ECHO* )==========================="
+        
+        {% if settings.fail_timing -%}
+        exit  1
+        {%- endif %}
+    }
+
+    puts "\n==========================( Writing netlists and SDF files )============================="
+    write_verilog -mode timesim -sdf_anno false -force -file ${outputs_dir}/timesim.v
+    write_sdf -mode timesim -process_corner slow -force -file ${outputs_dir}/timesim.min.sdf
+    write_sdf -mode timesim -process_corner fast -force -file ${outputs_dir}/timesim.max.sdf
+    write_vhdl -mode funcsim -include_xilinx_libs -write_all_overrides -force -file ${outputs_dir}/funcsim.vhdl
+    write_xdc -no_fixed_only -force ${outputs_dir}/impl.xdc
+    {# # write_verilog -mode timesim -sdf_anno false -include_xilinx_libs -write_all_overrides -force -file ${outputs_dir}/impl_timesim_inlined.v #}
+    {# # write_verilog -mode funcsim -force ${outputs_dir}/funcsim_noxlib.v #}
+}
+
+puts "\n==========================( Finished $ACTIVE_STEP reports )============================"
diff --git a/xeda/flows/vivado/templates/vivado_sim.tcl b/src/xeda/flows/vivado/templates/vivado_sim.tcl
similarity index 60%
rename from xeda/flows/vivado/templates/vivado_sim.tcl
rename to src/xeda/flows/vivado/templates/vivado_sim.tcl
index 0ac5bd06..c83a893d 100644
--- a/xeda/flows/vivado/templates/vivado_sim.tcl
+++ b/src/xeda/flows/vivado/templates/vivado_sim.tcl
@@ -5,23 +5,18 @@ proc errorExit {errorString} {
 }
 
 set design_name    {{design.name}}
-set snapshot_name  {{tb_top}}
+set snapshot_name  snapshot
 
 load_feature simulator
 
-set xelab_flags "-log xelab.log"
-{% if design.language.vhdl.standard == "93" %}
-append xelab_flags " -93_mode"
-{% endif %}
-
 if { [catch {file delete -force xsim.dir} error]} {
     puts "Failed to delete previously existing xsim.dir: $error"
 }
 
-set analyze_flags "-work {{lib_name}} {%- if debug %} -verbose 2 {%- endif %} {{analyze_flags}}"
+set analyze_flags "-work {{settings.work_lib}} {%- if settings.debug %} -verbose 2 {%- endif %} {{settings.analyze_flags|join(' ')}}"
 
 puts "\n===========================( Analyzing HDL Sources )==========================="
-{% for src in sim_sources %}
+{% for src in design.sim_sources %}
 {% if src.type == 'verilog' %}
 
 {% if src.variant == 'systemverilog' %}
@@ -45,79 +40,66 @@ if { [catch {eval exec xvhdl ${analyze_flags} {% if design.language.vhdl.standar
 {% endif %}
 {% endfor %}
 
-{% for rc in run_configs %}
-
 puts "\n===========================( Elaborating design )==========================="
-if { [catch {eval exec xelab -s ${snapshot_name} -L {{lib_name}} {{elab_flags}} ${xelab_flags} {%- for k,v in rc.generics.items() %} {{"-generic_top %s=%s"|format(k,v)}} {%- endfor %} {%- for top in sim_tops %} {{lib_name}}.{{top}} {% endfor -%}  } error]} {
+if { [catch {eval exec xelab -s ${snapshot_name} -L {{settings.work_lib}} {%- for l,_ in settings.lib_paths %} -L {{l}} {%- endfor %} {{settings.elab_flags|join(' ')}} {{settings.optimization_flags|join(' ')}} {% if settings.xelab_log %} -log {{settings.xelab_log}} {%- endif %} {%- for k,v in design.tb.parameters.items() %} {{"-generic_top %s=%s"|format(k,v)}} {%- endfor %} {%- for top in design.tb.top %} {{top}} {%- endfor -%}  } error]} {
     errorExit $error
 }
 
 puts "\n===========================( Loading Simulation )==========================="
-if { [catch {eval xsim ${snapshot_name} {{sim_flags}} } error] } {
+if { [catch {eval xsim ${snapshot_name} {{settings.sim_flags|join(' ')}} } error] } {
     errorExit $error
 }
 
-{% if rc.saif %}
+{% if settings.saif %}
 puts "\n===========================( Setting up SAIF )==========================="
-if {[file exists {{rc.saif}}]} {
-    puts "deleting existing SAIF file {{rc.saif}}"
-    file delete -force -- {{rc.saif}}
+if {[file exists {{settings.saif}}]} {
+    puts "deleting existing SAIF file {{settings.saif}}"
+    file delete -force -- {{settings.saif}}
 }
-open_saif {{rc.saif}}
+open_saif {{settings.saif}}
 {% endif %}
 
 ## TODO: WDB support
 ## set wdb_file "xsim_waves"
 ## open_wave_database ${wdb_file}
 
-{%- if rc.vcd %}
+{% if settings.vcd -%}
 puts "\n===========================( Setting up VCD )==========================="
-open_vcd {{rc.vcd}}
+open_vcd {{settings.vcd}}
 ## Vivado (tested on 2020.1) crashes if using * and shared/protected variables are present
-log_vcd [get_objects -r -filter { type == variable || type == signal || type == internal_signal || type == in_port || type == out_port || type == inout_port || type == port } *]
-{% endif -%}
-
-{%- if debug_traces %}
+log_vcd [get_objects -r -filter { type == variable || type == signal || type == internal_signal || type == in_port || type == out_port || type == inout_port || type == port } /*]
+{%- endif %}
+{% if settings.debug_traces -%}
 ltrace on
 ptrace on
-{% endif -%}
+{%- endif %}
 
 puts "\n===========================( Running simulation )==========================="
-puts "\n===========================( *ENABLE ECHO* )==========================="
-{% if flow.get('prerun_time') %}
-puts "Pre-run for {{flow.prerun_time}}"
-if { [catch {eval run {{flow.prerun_time}} } error]} {
+{% if settings.prerun_time -%}
+puts "Pre-run for {{settings.prerun_time}}"
+if { [catch {eval run {{settings.prerun_time}} } error]} {
     errorExit $error
 }
-{% endif -%}
+{%- endif %}
 
-{%- if rc.saif %}
+{% if settings.saif -%}
 puts "Adding nets to be logged in SAIF"
+log_saif [get_objects -r -filter { type == signal || type == internal_signal || type == in_port || type == out_port || type == inout_port || type == port } /{{design.tb.top[0]}}/{{design.tb.uut}}/*]
+{%- endif %}
 
-log_saif [get_objects -r -filter { type == signal || type == internal_signal || type == in_port || type == out_port || type == inout_port || type == port } /{{tb_top}}/{{design.tb.uut}}/*]
-{% endif -%}
-
-
-puts "Main Run\n"
-
-if { [catch {eval run {% if 'stop_time' in flow and flow.stop_time %} {{flow.stop_time}} {% else %} all {% endif %} } error]} {
+if { [catch {eval run {% if settings.stop_time -%} {{settings.stop_time}} {% else -%} all {%- endif %} } error]} {
     errorExit $error
 }
 
-set fin_time [eval current_time]
+puts "Vivado simulation finished at [eval current_time]"
 
-puts "\[Vivado\] Simulation finished at ${fin_time}"
-
-puts "\n===========================( *DISABLE ECHO* )==========================="
-{% if rc.vcd %}
+{% if settings.vcd %}
 puts "\n===========================( Closing VCD file )==========================="
 flush_vcd
 close_vcd
 {% endif -%}
 
-{%- if rc.saif %}
+{%- if settings.saif %}
 puts "\n===========================( Closing SAIF file )==========================="
 close_saif
 {% endif %}
-
-{% endfor %}
diff --git a/src/xeda/flows/vivado/templates/vivado_synth.tcl b/src/xeda/flows/vivado/templates/vivado_synth.tcl
new file mode 100644
index 00000000..372bc078
--- /dev/null
+++ b/src/xeda/flows/vivado/templates/vivado_synth.tcl
@@ -0,0 +1,103 @@
+# Auto-generated by Xeda
+
+create_project -part {{settings.fpga.part}} -force -verbose {{design.name}}
+
+set design_name           {{design.name}}
+set vhdl_std              {{design.language.vhdl.standard}}
+
+{% include 'util.tcl' %}
+
+set_param general.maxThreads {{settings.nthreads}}
+
+puts "\n=====================( Read Design Files and Constraints )====================="
+{% for src in design.rtl.sources -%}
+{% if src.type == 'systemverilog' -%}
+puts "Reading SystemVerilog file {{src.file}}"
+if { [catch {eval read_verilog -sv {{src.file}} } myError]} {
+    errorExit $myError
+}
+{%- elif src.type == 'verilog' %}
+puts "Reading Verilog file {{src.file}}"
+if { [catch {eval read_verilog {{src.file}} } myError]} {
+    errorExit $myError
+}
+{%- elif src.type == 'vhdl' %}
+puts "Reading VHDL file {{src.file}}"
+if { [catch {eval read_vhdl {% if design.language.vhdl.standard == "08" or design.language.vhdl.standard == "2008" %} -vhdl2008 {%- endif %} {{src.file}} } myError]} {
+    errorExit $myError
+}
+{%- endif %}
+{%- endfor %}
+
+{% for xdc_file in xdc_files %}
+read_xdc {{xdc_file}}
+{% endfor %}
+
+set_property top {{design.rtl.top}} [get_fileset sources_1]
+
+set avail_synth_strategies [join [list_property_value strategy [get_runs synth_1] ] " "]
+puts "available synthesis strategies: $avail_synth_strategies"
+
+{% if settings.synth.strategy %}
+puts "setting synthesis strategy to {{settings.synth.strategy}}"
+set_property strategy {{settings.synth.strategy}} [get_runs synth_1]
+{% endif %}
+
+
+set avail_impl_strategies [join [list_property_value strategy [get_runs impl_1] ] " "]
+puts "available implementation strategies: $avail_impl_strategies"
+
+{% if settings.impl.strategy -%}
+puts "setting implementation strategy to {{settings.impl.strategy}}"
+set_property strategy {{settings.impl.strategy}} [get_runs impl_1]
+{% endif -%}
+
+{% for k,v in design.rtl.parameters.items() -%}
+{% set x = ("1'b%d" % v) if v is boolean else v if v is not string else v if v is match("\\d+'b[01]+") else "\\\"" + v + "\\\"" -%}
+set_property generic {% raw -%} { {%- endraw -%} {{ k }}={{ x }} {%- raw -%} } {%- endraw %} [current_fileset]
+{% endfor -%}
+
+#{# see https://www.xilinx.com/support/documentation/sw_manuals/xilinx2020_2/ug912-vivado-properties.pdf #}
+#{# and https://www.xilinx.com/support/documentation/sw_manuals/xilinx2020_2/ug835-vivado-tcl-commands.pdf #}
+{% for step,options in settings.synth.steps.items() -%}
+{% for name,value in options.items() -%}
+{% if value is mapping %}
+{% for k,v in value.items() %}
+set_property STEPS.{{step}}.{{name}}.{{k}} {{v}} [get_runs synth_1]
+{% endfor -%}
+{% else %}
+set_property STEPS.{{step}}.{{name}} {{value}} [get_runs synth_1]
+{% endif %}
+{% endfor -%}
+{% endfor -%}
+
+{% for step,options in settings.impl.steps.items() -%}
+{% for name,value in options.items() -%}
+{% if value is mapping %}
+{% for k,v in value.items() %}
+set_property STEPS.{{step}}.{{name}}.{{k}} {{v}} [get_runs impl_1]
+{% endfor -%}
+{% else %}
+set_property STEPS.{{step}}.{{name}} {{value}} [get_runs impl_1]
+{% endif %}
+{% endfor -%}
+{% endfor -%}
+
+add_files -fileset utils_1 -norecurse [pwd]/{{reports_tcl}}
+set_property STEPS.ROUTE_DESIGN.TCL.POST [pwd]/{{reports_tcl}} [get_runs impl_1]
+
+create_report_config -report_type report_utilization -report_name post_opt_report        -steps opt_design   -runs [get_runs impl_1] -options {-format xml}
+create_report_config -report_type report_utilization -report_name post_opt_hier_report   -steps opt_design   -runs [get_runs impl_1] -options {-hierarchical -format xml}
+create_report_config -report_type report_utilization -report_name post_route_report      -steps route_design -runs [get_runs impl_1] -options {-format xml}
+create_report_config -report_type report_utilization -report_name post_route_hier_report -steps route_design -runs [get_runs impl_1] -options {-hierarchical -format xml}
+
+puts "\n=============================( Running Synthesis )============================="
+launch_runs synth_1 -jobs {{settings.nthreads}}
+wait_on_run synth_1
+# renamed to wait_on_runs in Vivado 2021.2?!!
+
+puts "\n===========================( Running Implementation )=========================="
+launch_runs impl_1 -jobs {{settings.nthreads}} {%- if not settings.write_bitstream %} -to_step route_design {%- endif %}
+wait_on_run impl_1
+
+puts "\n====================================( DONE )==================================="
diff --git a/src/xeda/flows/vivado/vivado_alt_synth.py b/src/xeda/flows/vivado/vivado_alt_synth.py
new file mode 100644
index 00000000..6c3c4477
--- /dev/null
+++ b/src/xeda/flows/vivado/vivado_alt_synth.py
@@ -0,0 +1,312 @@
+import logging
+from typing import Any, Dict
+
+from ..flow import FpgaSynthFlow
+from ...dataclass import validator
+from . import Vivado
+from .vivado_synth import RunOptions, VivadoSynth
+
+log = logging.getLogger(__name__)
+
+# curated options based on experiments and:
+# see https://www.xilinx.com/support/documentation/sw_manuals/xilinx2020_1/ug904-vivado-implementation.pdf
+# and https://www.xilinx.com/support/documentation/sw_manuals/xilinx2020_1/ug901-vivado-synthesis.pdf
+xeda_strategies: Dict[str, Dict[str, Any]] = {
+    "Debug": {
+        "synth": {
+            "-assert": None,
+            "-debug_log": None,
+            "-flatten_hierarchy": "none",
+            "-keep_equivalent_registers": None,
+            "-no_lc": None,
+            "-fsm_extraction": "off",
+            "-directive": "RuntimeOptimized",
+        },
+        "opt": {"-directive": "RuntimeOptimized"},
+        "place": {"-directive": "RuntimeOptimized"},
+        "place_opt": {},
+        "route": {"-directive": "RuntimeOptimized"},
+        "phys_opt": {"-directive": "RuntimeOptimized"},
+    },
+    "Runtime": {
+        "synth": {"-directive": "RuntimeOptimized"},
+        "opt": {"-directive": "RuntimeOptimized"},
+        "place": {"-directive": "RuntimeOptimized"},
+        "place_opt": {},
+        # with -ultrathreads results are not reproducible!
+        # OR "-no_timing_driven -ultrathreads",
+        "route": {"-directive": "RuntimeOptimized"},
+        "phys_opt": {"-directive": "RuntimeOptimized"},
+    },
+    "Default": {
+        "synth": {"-flatten_hierarchy": "rebuilt", "-directive": "Default"},
+        "opt": {"-directive": "ExploreWithRemap"},
+        "place": {"-directive": "Default"},
+        "place_opt": {},
+        "route": {"-directive": "Default"},
+        "phys_opt": {"-directive": "Default"},
+    },
+    "Timing": {
+        # -mode: default, out_of_context
+        # -flatten_hierarchy: rebuilt, full; equivalent in terms of QoR?
+        # -no_lc: When checked, this option turns off LUT combining
+        # -keep_equivalent_registers -no_lc
+        "synth": {
+            "-flatten_hierarchy": "rebuilt",
+            "-retiming": None,
+            "-directive": "PerformanceOptimized",
+            "-fsm_extraction": "one_hot",
+            #   "-resource_sharing off",
+            #   "-no_lc",
+            "-shreg_min_size": "5",
+            #   "-keep_equivalent_registers "
+        },
+        "opt": {"-directive": "ExploreWithRemap"},
+        "place": {"-directive": "ExtraPostPlacementOpt"},
+        "place_opt": {
+            "-retarget": None,
+            "-propconst": None,
+            "-sweep": None,
+            "-aggressive_remap": None,
+            "-shift_register_opt": None,
+        },
+        "phys_opt": {"-directive": "AggressiveExplore"},
+        "place_opt2": {"-directive": "Explore"},
+        # "route": "-directive NoTimingRelaxation",
+        "route": {"-directive": "AggressiveExplore"},
+    },
+    # "ExtraTimingCongestion": {
+    #     "synth": ["-flatten_hierarchy": "full",
+    #               "-retiming",
+    #               "-directive": "PerformanceOptimized",
+    #               "-fsm_extraction": "one_hot",
+    #               "-resource_sharing off",
+    #               "-shreg_min_size 10",
+    #               "-keep_equivalent_registers",
+    #               ],
+    #     "opt": ["-directive ExploreWithRemap"],
+    #     "place": ["-directive AltSpreadLogic_high"],
+    #     "place_opt": ['-retarget', '-propconst', '-sweep', '-remap', '-muxf_remap', '-aggressive_remap', '-shift_register_opt'],
+    #     "place_opt2": ["-directive Explore"],
+    #     "phys_opt": ["-directive AggressiveExplore"],
+    #     "route": ["-directive AlternateCLBRouting"],
+    # },
+    "ExtraTiming": {
+        "synth": {
+            "-flatten_hierarchy": "full",
+            "-retiming": None,
+            "-directive": "PerformanceOptimized",
+            "-fsm_extraction": "one_hot",
+            "-resource_sharing": "off",
+            #   "-no_lc",
+            "-shreg_min_size": "10",
+            "-keep_equivalent_registers": None,
+        },
+        "opt": {"-directive": "ExploreWithRemap"},
+        "place": {"-directive": "ExtraTimingOpt"},
+        "place_opt": {
+            "-retarget": None,
+            "-propconst": None,
+            "-sweep": None,
+            "-muxf_remap": None,
+            "-aggressive_remap": None,
+            "-shift_register_opt": None,
+        },
+        "place_opt2": {"-directive": "Explore"},
+        "phys_opt": {"-directive": "AggressiveExplore"},
+        "route": {"-directive": "NoTimingRelaxation"},
+    },
+    # "ExtraTimingAltRouting": {
+    #     # -mode: default, out_of_context
+    #     # -flatten_hierarchy: rebuilt, full; equivalent in terms of QoR?
+    #     # -no_lc: When checked, this option turns off LUT combining
+    #     # -keep_equivalent_registers -no_lc
+    #     "synth": ["-flatten_hierarchy full",
+    #               "-retiming",
+    #               "-directive PerformanceOptimized",
+    #               "-fsm_extraction one_hot",
+    #               #   "-resource_sharing off",
+    #               #   "-no_lc",
+    #               "-shreg_min_size 5",
+    #               "-keep_equivalent_registers "
+    #               ],
+    #     "opt": ["-directive ExploreWithRemap"],
+    #     "place": ["-directive ExtraTimingOpt"],
+    #     "place_opt": ['-retarget', '-propconst', '-sweep', '-aggressive_remap', '-shift_register_opt'],
+    #     "phys_opt": ["-directive AggressiveExplore"],
+    #     # "route": "-directive NoTimingRelaxation",
+    #     "route": ["-directive AlternateCLBRouting"],
+    # },
+    # "Area": {
+    #     # AreaOptimized_medium or _high prints error messages in Vivado 2020.1: "unexpected non-zero reference counts", but succeeeds and post-impl sim is OK too
+    #     "synth": ["-flatten_hierarchy full", "-control_set_opt_threshold 1", "-shreg_min_size 3", "-resource_sharing auto", "-directive AreaOptimized_medium"],
+    #     # if no directive: -resynth_seq_area
+    #     "opt": "-directive ExploreArea",
+    #     "place": "-directive Default",
+    #     "place_opt": "-directive ExploreArea",
+    #     # "place_opt": ['-retarget', '-propconst', '-sweep', '-aggressive_remap', '-shift_register_opt',
+    #     #               '-dsp_register_opt', '-bram_power_opt', '-resynth_seq_area', '-merge_equivalent_drivers'],
+    #     # if no directive: -placement_opt
+    #     "phys_opt": "-directive Explore",
+    #     "route": "-directive Explore",
+    # },
+    # "AreaHigh": {
+    #     # AreaOptimized_medium or _high prints error messages in Vivado 2020.1: "unexpected non-zero reference counts", but succeeeds and post-impl sim is OK too
+    #     "synth": ["-flatten_hierarchy full", "-control_set_opt_threshold 1", "-shreg_min_size 3", "-resource_sharing on", "-directive AreaOptimized_high"],
+    #     # if no directive: -resynth_seq_area
+    #     "opt": "-directive ExploreArea",
+    #     "place": "-directive Default",
+    #     "place_opt": "-directive ExploreArea",
+    #     # "place_opt": ['-retarget', '-propconst', '-sweep', '-aggressive_remap', '-shift_register_opt',
+    #     #               '-dsp_register_opt', '-bram_power_opt', '-resynth_seq_area', '-merge_equivalent_drivers'],
+    #     # if no directive: -placement_opt
+    #     "phys_opt": "-directive Explore",
+    #     "route": "-directive Explore",
+    # },
+    # "AreaPower": {
+    #     # AreaOptimized_medium or _high prints error messages in Vivado 2020.1: "unexpected non-zero reference counts", but succeeeds and post-impl sim is OK too
+    #     "synth": ["-flatten_hierarchy full", "-control_set_opt_threshold 1", "-shreg_min_size 3", "-resource_sharing auto", "-gated_clock_conversion auto", "-directive AreaOptimized_medium"],
+    #     "opt": ["-directive ExploreArea"],
+    #     "place": "-directive Default",
+    #     "place_opt": ['-retarget', '-propconst', '-sweep', '-aggressive_remap', '-shift_register_opt', '-dsp_register_opt', '-resynth_seq_area', '-merge_equivalent_drivers'],
+    #     "place_opt2": ["-directive ExploreArea"],
+    #     # FIXME!!! This is the only option that results in correct post-impl timing sim! Why??!
+    #     "phys_opt": ["-directive AggressiveExplore"],
+    #     "route": ["-directive Explore"],
+    # },
+    # "AreaTiming": {
+    #     "synth": ["-flatten_hierarchy rebuilt", "-retiming"],
+    #     # if no directive: -resynth_seq_area
+    #     "opt": ["-directive ExploreWithRemap"],
+    #     "place": ["-directive ExtraPostPlacementOpt"],
+    #     # "place_opt": ["-directive ExploreArea"],
+    #     "place_opt": ['-retarget', '-propconst', '-sweep', '-aggressive_remap', '-shift_register_opt', '-dsp_register_opt', '-resynth_seq_area', '-merge_equivalent_drivers'],
+    #     "place_opt2": ["-directive ExploreArea"],
+    #     # if no directive: -placement_opt
+    #     "phys_opt": "-directive AggressiveExplore",
+    #     "route": "-directive Explore",
+    # },
+    # "AreaExploreWithRemap": {
+    #     "synth": ["-flatten_hierarchy full", "-retiming"],
+    #     # if no directive: -resynth_seq_area
+    #     "opt": "-directive ExploreWithRemap",
+    #     "place": "-directive Default",
+    #     "place_opt": "-directive ExploreWithRemap",
+    #     # "place_opt": ['-retarget', '-propconst', '-sweep', '-aggressive_remap', '-shift_register_opt',
+    #     #               '-dsp_register_opt', '-bram_power_opt', '-resynth_seq_area', '-merge_equivalent_drivers'],
+    #     # if no directive: -placement_opt
+    #     "phys_opt": "-directive Explore",
+    #     "route": "-directive Explore",
+    # },
+    # "AreaExploreWithRemap2": {
+    #     "synth": [],
+    #     # if no directive: -resynth_seq_area
+    #     "opt": "-directive ExploreArea",
+    #     "place": "-directive Default",
+    #     "place_opt": "-directive ExploreWithRemap",
+    #     # "place_opt": ['-retarget', '-propconst', '-sweep', '-aggressive_remap', '-shift_register_opt',
+    #     #               '-dsp_register_opt', '-bram_power_opt', '-resynth_seq_area', '-merge_equivalent_drivers'],
+    #     # if no directive: -placement_opt
+    #     "phys_opt": "-directive Explore",
+    #     "route": "-directive Explore",
+    # },
+    # "AreaExplore": {
+    #     "synth": ["-flatten_hierarchy full"],
+    #     # if no directive: -resynth_seq_area
+    #     "opt": "-directive ExploreArea",
+    #     "place": "-directive Default",
+    #     "place_opt": "-directive ExploreArea",
+    #     # "place_opt": ['-retarget', '-propconst', '-sweep', '-aggressive_remap', '-shift_register_opt',
+    #     #               '-dsp_register_opt', '-bram_power_opt', '-resynth_seq_area', '-merge_equivalent_drivers'],
+    #     # if no directive: -placement_opt
+    #     "phys_opt": "-directive Explore",
+    #     "route": "-directive Explore",
+    # },
+    # "Power": {
+    #     "synth": ["-flatten_hierarchy full", "-gated_clock_conversion auto", "-control_set_opt_threshold 1", "-shreg_min_size 3", "-resource_sharing auto"],
+    #     # if no directive: -resynth_seq_area
+    #     "opt": "-directive ExploreSequentialArea",
+    #     "place": "-directive Default",
+    #     "place_opt": "-directive ExploreSequentialArea",
+    #     # ['-retarget', '-propconst', '-sweep', '-aggressive_remap', '-shift_register_opt',
+    #     #   '-dsp_register_opt', '-bram_power_opt', '-resynth_seq_area', '-merge_equivalent_drivers'],
+    #     # if no directive: -placement_opt
+    #     "phys_opt": "-directive Explore",
+    #     "route": "-directive Explore",
+    # }
+}
+
+
+def _vivado_steps(strategy: str, s: str):
+    if s == "synth":
+        steps = ["synth", "opt"]
+    else:
+        steps = [
+            "place",
+            "place_opt",
+            "place_opt2",
+            "phys_opt",
+            "phys_opt2",
+            "route",
+        ]
+    return {step: xeda_strategies[strategy].get(step) for step in steps}
+
+
+class VivadoAltSynth(Vivado, FpgaSynthFlow):
+    """Synthesize with Xilinx Vivado using an alternative TCL-based flow"""
+
+    class Settings(VivadoSynth.Settings):
+        synth: RunOptions = RunOptions(
+            strategy="Default", steps=_vivado_steps("Default", "synth")
+        )
+        impl: RunOptions = RunOptions(
+            strategy="Default", steps=_vivado_steps("Default", "impl")
+        )
+
+        # pylint: disable=no-self-argument,no-self-use
+        @validator("synth")
+        def validate_synth(cls, v: RunOptions):
+            if v.strategy and not v.steps:
+                v.steps = _vivado_steps(v.strategy, "synth")
+            return v
+
+        # pylint: disable=no-self-argument,no-self-use
+        @validator("impl", always=True)
+        def validate_impl(cls, v: RunOptions, values):
+            if not v.strategy:
+                v.strategy = values.get("synth", {}).get("strategy")
+            if v.strategy and not v.steps:
+                v.steps = _vivado_steps(v.strategy, "impl")
+            return v
+
+    def run(self):
+        clock_xdc_path = self.copy_from_template("clock.xdc")
+        ss = self.settings
+        assert isinstance(ss, self.Settings)
+
+        if ss.out_of_context:
+            if "synth" in ss.synth.steps and ss.synth.steps["synth"] is not None:
+                ss.synth.steps["synth"]["-mode"] = "out_of_context"
+
+        blacklisted_resources = ss.blacklisted_resources
+        if "bram" in blacklisted_resources and "bram_tile" not in blacklisted_resources:
+            blacklisted_resources.append("bram_tile")
+
+        log.info("blacklisted_resources: %s", blacklisted_resources)
+
+        # if 'bram_tile' in blacklisted_resources:
+        #     # FIXME also add -max_uram 0 for ultrascale+
+        #     settings.synth.steps['synth'].append('-max_bram 0')
+        # if 'dsp' in blacklisted_resources:
+        #     settings.synth.steps['synth'].append('-max_dsp 0')
+
+        self.add_template_filter(
+            "flatten_dict",
+            lambda d: " ".join([f"{k} {v}" if v else k for k, v in d.items()]),
+        )
+
+        script_path = self.copy_from_template(
+            "vivado_alt_synth.tcl",
+            xdc_files=[clock_xdc_path],
+        )
+        self.vivado.run("-source", script_path)
diff --git a/src/xeda/flows/vivado/vivado_postsynthsim.py b/src/xeda/flows/vivado/vivado_postsynthsim.py
new file mode 100644
index 00000000..7fe8055b
--- /dev/null
+++ b/src/xeda/flows/vivado/vivado_postsynthsim.py
@@ -0,0 +1,79 @@
+import logging
+
+from xeda.flows.flow import FlowFatalError
+
+from ...design import DesignSource, RtlSettings
+from ...utils import SDF
+from .vivado_sim import VivadoSim
+from .vivado_synth import VivadoSynth
+
+log = logging.getLogger(__name__)
+
+
+class VivadoPostsynthSim(VivadoSim):
+    """
+    Synthesizes & implements the design, then runs post-synthesis/post-implementation simulation on the generated netlist.
+    The netlist can be optionally annotated with generated timing information (SDF).
+    Depends on VivadoSynth
+    """
+
+    class Settings(VivadoSim.Settings):
+        synth: VivadoSynth.Settings
+        timing_sim: bool = False
+        enforce_io_delay: bool = True
+
+    def init(self) -> None:
+        ss = self.settings
+        assert isinstance(ss, self.Settings)
+        if ss.enforce_io_delay:
+            # Force 0 I/O delay if no I/O delay is given
+            # This seems to be required to meet timing
+            if ss.synth.input_delay is None:
+                ss.synth.input_delay = 0.0
+            if ss.synth.output_delay is None:
+                ss.synth.output_delay = 0.0
+        self.add_dependency(VivadoSynth, ss.synth)
+
+    def run(self) -> None:
+        synth_flow = self.completed_dependencies[0]
+        assert isinstance(synth_flow, VivadoSynth)
+        ss = self.settings
+        assert isinstance(ss, self.Settings)
+
+        artifacts_path = (
+            synth_flow.run_path / synth_flow.settings.outputs_dir / "route_design"
+        )
+        synth_netlist_path = artifacts_path / "timesim.v"
+        if not synth_netlist_path.exists():
+            raise FlowFatalError(f"Netlist {synth_netlist_path} does not exist!")
+        postsynth_sources = [DesignSource(synth_netlist_path)]
+        log.info("Setting post-synthesis sources to: %s", postsynth_sources)
+        # also removing top-level generics and everything else
+        self.design.rtl = RtlSettings(
+            top=self.design.rtl.top, sources=postsynth_sources
+        )
+        assert self.design.tb and self.design.tb.top
+        self.design.tb.top = (self.design.tb.top[0], "glbl")
+
+        if "simprims_ver" not in ss.lib_paths:
+            ss.lib_paths.append(("simprims_ver", None))
+
+        if ss.timing_sim:
+            if not ss.sdf.delay_items():
+                ss.sdf = SDF(max=str(artifacts_path / "timesim.max.sdf"))
+            if not ss.sdf.root:
+                ss.sdf.root = self.design.tb.uut
+            log.info("Timing simulation using SDF %s", ss.sdf)
+
+        ss.elab_flags.extend(
+            [
+                "-maxdelay",
+                "-transport_int_delays",
+                "-pulse_r 0",
+                "-pulse_int_r 0",
+                "-pulse_e 0",
+                "-pulse_int_e 0",
+            ]
+        )
+        # run VivadoSim
+        super().run()
diff --git a/src/xeda/flows/vivado/vivado_power.py b/src/xeda/flows/vivado/vivado_power.py
new file mode 100644
index 00000000..ffcd7ef1
--- /dev/null
+++ b/src/xeda/flows/vivado/vivado_power.py
@@ -0,0 +1,65 @@
+import html
+import logging
+from typing import Any, Dict
+from xml.etree import ElementTree
+
+from .vivado_postsynthsim import VivadoPostsynthSim
+from .vivado_alt_synth import VivadoSynth
+
+logger = logging.getLogger(__name__)
+
+
+class VivadoPower(VivadoPostsynthSim):
+    class Settings(VivadoPostsynthSim.Settings):
+        saif: str = "activity.saif"
+        elab_debug = "typical"
+        timing_sim = True
+        power_report_xml: str = "power_impl_timing.xml"
+
+    def run(self) -> None:
+        super().run()
+
+        assert isinstance(self.settings, self.Settings)
+        dep_synth_flow = self.pop_dependency(VivadoSynth)
+        assert self.design.tb
+        # assert isinstance(dep_synth_flow.settings, VivadoSynth.Settings)
+        top = self.design.rtl.top
+        script_path = self.copy_from_template(
+            "vivado_power.tcl",
+            checkpoint=str(
+                dep_synth_flow.run_path / f"{top}.runs" / "impl_1" / f"{top}_routed.dcp"
+            ),
+        )
+
+        self.vivado.run("-source", script_path)
+
+    def parse_power_report(self, report_xml) -> Dict[str, Any]:
+        tree = ElementTree.parse(report_xml)
+        results = {}
+        for tablerow in tree.findall("./section[@title='Summary']/table/tablerow"):
+            tablecells = tablerow.findall("tablecell")
+            key, value = (
+                html.unescape(x.attrib["contents"]).strip() for x in tablecells
+            )
+            results[key] = value
+
+        for tablerow in tree.findall(
+            "./section[@title='Summary']/section[@title='On-Chip Components']/table/tablerow"
+        ):
+            tablecells = tablerow.findall("tablecell")
+            if len(tablecells) >= 2:
+                contents = [
+                    html.unescape(x.attrib["contents"]).strip() for x in tablecells
+                ]
+                key = contents[0]
+                value = contents[1]
+                results[f"Component Power: {key}"] = value
+
+        return results
+
+    def parse_reports(self) -> bool:
+        assert isinstance(self.settings, self.Settings)
+        report_xml = self.run_path / self.settings.power_report_xml
+        results = self.parse_power_report(report_xml)
+        self.results.update(**results)
+        return True
diff --git a/src/xeda/flows/vivado/vivado_sim.py b/src/xeda/flows/vivado/vivado_sim.py
new file mode 100644
index 00000000..fc3aadd2
--- /dev/null
+++ b/src/xeda/flows/vivado/vivado_sim.py
@@ -0,0 +1,59 @@
+import logging
+from typing import List, Optional
+
+from ...dataclass import Field
+from ...utils import SDF
+from ..flow import SimFlow
+from ..vivado import Vivado
+
+log = logging.getLogger(__name__)
+
+
+# FIXME: Does not return error when simulation is finished with a failure assertion
+
+
+class VivadoSim(Vivado, SimFlow):
+    """Simulate using Xilinx Vivado simulator (xsim) flow"""
+
+    # TODO change this?
+    # Can run multiple configurations (a.k.a testvectors) in a single run of Vivado through "run_configs"
+
+    class Settings(Vivado.Settings, SimFlow.Settings):
+        saif: Optional[str] = None
+        elab_flags: List[str] = ["-relax"]
+        analyze_flags: List[str] = ["-relax"]
+        sim_flags: List[str] = []
+        elab_debug: Optional[str] = None  # TODO choices: "typical", ...
+        sdf: SDF = SDF()
+        optimization_flags: List[str] = []  # ["-O3"]
+        debug_traces: bool = False
+        prerun_time: Optional[str] = None
+        work_lib: str = "work"
+        initialize_zeros: bool = Field(
+            False, description="Initialize all signals with zero"
+        )
+        xelab_log: Optional[str] = "xeda_xelab.log"
+
+    def run(self) -> None:
+        ss = self.settings
+        assert isinstance(ss, self.Settings)
+
+        ss.elab_flags.append(f'-mt {"off" if ss.debug else ss.nthreads}')
+        elab_debug = ss.elab_debug
+        if not elab_debug and (ss.debug or ss.saif or ss.vcd):
+            elab_debug = "typical"
+        if elab_debug:
+            ss.elab_flags.append(f"-debug {elab_debug}")
+
+        assert self.design.tb
+        if ss.vcd:
+            log.info("Dumping VCD to %s", self.run_path / ss.vcd)
+        sdf_root = ss.sdf.root
+        if not sdf_root:
+            sdf_root = self.design.tb.uut
+        for delay_type, sdf_file in ss.sdf.delay_items():
+            assert sdf_root, "neither SDF root nor tb.uut are provided"
+            ss.elab_flags.append(f"-sdf{delay_type} {sdf_root}={sdf_file}")
+
+        script_path = self.copy_from_template("vivado_sim.tcl")
+        self.vivado.run("-source", script_path)
diff --git a/src/xeda/flows/vivado/vivado_synth.py b/src/xeda/flows/vivado/vivado_synth.py
new file mode 100644
index 00000000..7e702ee7
--- /dev/null
+++ b/src/xeda/flows/vivado/vivado_synth.py
@@ -0,0 +1,251 @@
+import logging
+import os
+from pathlib import Path
+from typing import Any, Dict, List, Optional
+
+from ...dataclass import Field, XedaBaseModel
+from ..flow import FpgaSynthFlow
+from ..vivado import Vivado
+
+log = logging.getLogger(__name__)
+
+
+StepsValType = Optional[Dict[str, Any]]
+
+
+class RunOptions(XedaBaseModel):
+    strategy: Optional[str] = None
+    steps: Dict[str, StepsValType] = {}
+
+
+class VivadoSynth(Vivado, FpgaSynthFlow):
+    """Synthesize with Xilinx Vivado using a project-based flow"""
+
+    class Settings(Vivado.Settings, FpgaSynthFlow.Settings):
+        """Settings for Vivado synthesis in project mode"""
+
+        # FIXME implement and verify all
+        checkpoints_dir: str = "checkpoints"
+        fail_critical_warning = Field(
+            False,
+            description="flow fails if any Critical Warnings are reported by Vivado",
+        )
+        fail_timing = Field(True, description="flow fails if timing is not met")
+        optimize_power = False
+        optimize_power_postplace = False
+        blacklisted_resources: List[str] = Field(  # TODO: remove
+            # ["latch"],
+            [],
+            description="list of FPGA resources which are not allowed to be inferred or exist in the results. Valid values: latch, dsp, bram",
+        )
+        input_delay: Optional[float] = None
+        output_delay: Optional[float] = None
+        out_of_context: bool = False
+        write_checkpoint: bool = False
+        write_netlist: bool = True
+        write_bitstream: bool = False
+        qor_suggestions: bool = False
+        synth: RunOptions = RunOptions(
+            strategy="Flow_PerfOptimized_high",
+            steps={
+                "SYNTH_DESIGN": {},
+                "OPT_DESIGN": {},
+                "POWER_OPT_DESIGN": {},
+            },
+        )
+        impl: RunOptions = RunOptions(
+            strategy="Performance_ExploreWithRemap",
+            steps={
+                "PLACE_DESIGN": {},
+                "POST_PLACE_POWER_OPT_DESIGN": {},
+                "PHYS_OPT_DESIGN": {},
+                "ROUTE_DESIGN": {},
+                "WRITE_BITSTREAM": {},
+            },
+        )
+
+    def run(self):
+        assert isinstance(self.settings, self.Settings)
+        settings = self.settings
+        for o in [
+            "timesim.min.sdf",
+            "timesim.max.sdf",
+            "timesim.v",
+            "funcsim.vhdl",
+            "xdc",
+        ]:
+            self.artifacts[o] = os.path.join(settings.outputs_dir, o)
+
+        settings.synth.steps = {
+            **{
+                "SYNTH_DESIGN": {},
+                "OPT_DESIGN": {},
+                "POWER_OPT_DESIGN": {},
+            },
+            **settings.synth.steps,
+        }
+        settings.impl.steps = {
+            **{
+                "PLACE_DESIGN": {},
+                "POST_PLACE_POWER_OPT_DESIGN": {},
+                "PHYS_OPT_DESIGN": {},
+                "ROUTE_DESIGN": {},
+                "WRITE_BITSTREAM": {},
+            },
+            **settings.impl.steps,
+        }
+
+        if not self.design.rtl.clock_port:
+            log.critical(
+                "No clocks specified for top RTL design. Continuing with synthesis anyways."
+            )
+        else:
+            assert (
+                settings.clock_period
+            ), "`clock_period` must be specified and be positive value"
+            freq = 1000 / settings.clock_period
+            log.info(
+                "clock.port=%s clock.frequency=%.3f MHz",
+                self.design.rtl.clock_port,
+                freq,
+            )
+        clock_xdc_path = self.copy_from_template("clock.xdc")
+
+        if settings.blacklisted_resources:
+            log.info("blacklisted_resources: %s", self.settings.blacklisted_resources)
+
+        if settings.synth.steps["SYNTH_DESIGN"] is None:
+            settings.synth.steps["SYNTH_DESIGN"] = {}
+        assert settings.synth.steps["SYNTH_DESIGN"] is not None
+        if "bram_tile" in settings.blacklisted_resources:
+            # FIXME also add -max_uram 0 for ultrascale+
+            settings.synth.steps["SYNTH_DESIGN"]["MAX_BRAM"] = 0
+        if "dsp" in settings.blacklisted_resources:
+            settings.synth.steps["SYNTH_DESIGN"]["MAX_DSP"] = 0
+
+        reports_tcl = self.copy_from_template("vivado_report_helper.tcl")
+        script_path = self.copy_from_template(
+            "vivado_synth.tcl", xdc_files=[clock_xdc_path], reports_tcl=reports_tcl
+        )
+        self.vivado.run("-source", script_path)
+
+    def parse_timing_report(self, reports_dir) -> bool:
+        failed = False
+        assert isinstance(self.settings, self.Settings)
+        if self.design.rtl.clock_port and self.settings.clock_period:
+            failed |= not self.parse_report_regex(
+                reports_dir / "timing_summary.rpt",
+                ##
+                r"Timing\s+Summary[\s\|\-]+WNS\(ns\)\s+TNS\(ns\)\s+"
+                r"TNS Failing Endpoints\s+TNS Total Endpoints\s+WHS\(ns\)\s+THS\(ns\)\s+"
+                r"THS Failing Endpoints\s+THS Total Endpoints\s+WPWS\(ns\)\s+TPWS\(ns\)\s+"
+                r"TPWS Failing Endpoints\s+TPWS Total Endpoints\s*"
+                r"\s*(?:\-+\s+)+"
+                r"(?P<wns>\-?\d+(?:\.\d+)?)\s+(?P<_tns>\-?\d+(?:\.\d+)?)\s+(?P<_failing_endpoints>\d+)\s+(?P<_tns_total_endpoints>\d+)\s+"
+                r"(?P<whs>\-?\d+(?:\.\d+)?)\s+(?P<_ths>\-?\d+(?:\.\d+)?)\s+(?P<_ths_failing_endpoints>\d+)\s+(?P<_ths_total_endpoints>\d+)\s+",
+                ##
+                r"Clock Summary[\s\|\-]+^\s*Clock\s+.*$[^\w]+(\w*)\s+(\{.*\})\s+(?P<clock_period>\d+(?:\.\d+)?)\s+(?P<clock_frequency>\d+(?:\.\d+)?)",
+                required=False,
+            )
+            wns = self.results.get("wns")
+            if wns:
+                if not isinstance(wns, (float, int)):
+                    log.critical("Parse value for `WNS` is %s (%s)", wns, type(wns))
+                else:
+                    if wns < 0:
+                        failed = True
+                    # see https://support.xilinx.com/s/article/57304?language=en_US
+                    # Fmax in Megahertz
+                    # Here Fmax refers to: "The maximum frequency a design can run on Hardware in a given implementation
+                    # Fmax = 1/(T-WNS), with WNS positive or negative, where T is the target clock period."
+                    if "clock_period" in self.results:
+                        clock_period = self.results["clock_period"]
+                        assert isinstance(
+                            clock_period, (float, int)
+                        ), f"clock_period: {clock_period} is not a number"
+                        self.results["Fmax"] = 1000.0 / (clock_period - wns)
+
+        return not failed
+
+    def parse_reports(self) -> bool:
+        report_stage = "route_design"
+        reports_dir = Path("reports") / report_stage
+
+        failed = not self.parse_timing_report(reports_dir)
+
+        report_file = reports_dir / "utilization.xml"
+        utilization = self.parse_xml_report(report_file)
+        # ordered dict
+        fields = [
+            ("slice", ["Slice Logic Distribution", "Slice"]),
+            ("slice", ["CLB Logic Distribution", "CLB"]),  # Ultrascale+
+            ("lut", ["Slice Logic", "Slice LUTs"]),
+            ("lut", ["Slice Logic", "Slice LUTs*"]),
+            ("lut", ["CLB Logic", "CLB LUTs"]),
+            ("lut", ["CLB Logic", "CLB LUTs*"]),
+            ("lut_logic", ["Slice Logic", "LUT as Logic"]),
+            ("lut_logic", ["CLB Logic", "LUT as Logic"]),
+            ("lut_mem", ["Slice Logic", "LUT as Memory"]),
+            ("lut_mem", ["CLB Logic", "LUT as Memory"]),
+            ("ff", ["Slice Logic", "Register as Flip Flop"]),
+            ("ff", ["CLB Logic", "CLB Registers"]),
+            ("latch", ["Slice Logic", "Register as Latch"]),
+            ("latch", ["CLB Logic", "Register as Latch"]),
+            ("bram_tile", ["Memory", "Block RAM Tile"]),
+            ("bram_tile", ["BLOCKRAM", "Block RAM Tile"]),
+            ("bram_RAMB36", ["Memory", "RAMB36/FIFO*"]),
+            ("bram_RAMB36", ["BLOCKRAM", "RAMB36/FIFO*"]),
+            ("bram_RAMB18", ["Memory", "RAMB18"]),
+            ("bram_RAMB18", ["BLOCKRAM", "RAMB18"]),
+            ("dsp", ["DSP", "DSPs"]),
+            ("dsp", ["ARITHMETIC", "DSPs"]),
+        ]
+        if utilization is not None:
+            for k, path in fields:
+                if self.results.get(k) is None:
+                    path.append("Used")
+                    try:
+                        self.results[k] = self.get_from_path(utilization, path)
+                    except KeyError as e:
+                        log.debug(
+                            "determining %s: property %s (in %s) was not found in the utilization report %s.",
+                            k,
+                            e.args[0] if len(e.args) > 0 else "?",
+                            path,
+                            report_file,
+                        )
+
+        self.results["_utilization"] = utilization
+
+        assert isinstance(self.settings, self.Settings)
+
+        if not failed:
+            for res in self.settings.blacklisted_resources:
+                res_util = self.results.get(res)
+                if res_util is not None:
+                    try:
+                        res_util = int(res_util)
+                        if res_util > 0:
+                            log.critical(
+                                "%s utilization report lists %s use(s) of blacklisted resource: %s",
+                                report_stage,
+                                res_util,
+                                res,
+                            )
+                            failed = True
+                    except ValueError:
+                        log.warning(
+                            "Unknown utilization value: %s for blacklisted resource %s. Assuming results are not violating the blacklist criteria.",
+                            res_util,
+                            res,
+                        )
+
+            # TODO better fail analysis for vivado
+            if "wns" in self.results:
+                failed |= self.results["wns"] < 0
+            if "whs" in self.results:
+                failed |= self.results["whs"] < 0
+            if "_failing_endpoints" in self.results:
+                failed |= self.results["_failing_endpoints"] != 0
+
+        return not failed
diff --git a/src/xeda/flows/yosys/__init__.py b/src/xeda/flows/yosys/__init__.py
new file mode 100644
index 00000000..091da709
--- /dev/null
+++ b/src/xeda/flows/yosys/__init__.py
@@ -0,0 +1,278 @@
+import json
+import logging
+from pathlib import Path
+from typing import Any, Dict, List, Literal, Optional
+
+from box import Box
+
+from ...dataclass import Field, root_validator, validator
+from ...design import Design
+from ...flows.ghdl import GhdlSynth
+from ...tool import Docker, Tool
+from ..flow import AsicSynthFlow, FpgaSynthFlow, SynthFlow
+
+log = logging.getLogger(__name__)
+
+
+def append_flag(flag_list: List[str], flag: str) -> List[str]:
+    if flag not in flag_list:
+        flag_list.append(flag)
+    return flag_list
+
+
+class Yosys(SynthFlow):
+    """Synthesize the design using Yosys Open SYnthesis Suite"""
+
+    class Settings(FpgaSynthFlow.Settings, AsicSynthFlow.Settings):
+        log_file: Optional[str] = "yosys.log"
+        flatten: bool = Field(True, description="flatten design")
+        abc9: bool = Field(True, description="Use abc9")
+        retime: bool = Field(False, description="Enable flip-flop retiming")
+        nobram: bool = Field(False, description="Do not map to block RAM cells")
+        nodsp: bool = Field(False, description="Do not use DSP resources")
+        nolutram: bool = Field(False, description="Do not use LUT RAM cells")
+        sta: bool = Field(
+            False,
+            description="Run a simple static timing analysis (requires `flatten`)",
+        )
+        nowidelut: bool = Field(
+            False,
+            description="Do not use MUX resources to implement LUTs larger than native for the target",
+        )
+        dff: bool = Field(True, description="Run abc/abc9 with -dff option")
+        widemux: int = Field(
+            0,
+            description="enable inference of hard multiplexer resources for muxes at or above this number of inputs"
+            " (minimum value 2, recommended value >= 5 or disabled = 0)",
+        )
+        read_verilog_flags: List[str] = [
+            "-noautowire",
+        ]
+        read_liberty_flags: List[str] = []
+        synth_flags: List[str] = []
+        abc_flags: List[str] = []
+        write_vhdl_flags: List[str] = [
+            # '-norename',
+            # '-std08'.
+            # '-noattr',
+            "-renameprefix YOSYS_n",
+            # "-v",
+        ]
+        write_verilog_flags: List[str] = [
+            # '-norename',
+            # '-attr2comment',
+            # '-noexpr',
+            # '-siminit',
+            # '-extmem',
+            # '-sv',
+        ]
+        check_assert: bool = True
+        netlist_verilog: Optional[str] = "netlist.v"
+        netlist_vhdl: Optional[str] = None  # "netlist.vhdl"
+        netlist_json: Optional[str] = "netlist.json"
+        rtl_verilog: Optional[str] = None  # "rtl.v"
+        rtl_vhdl: Optional[str] = None  # "rtl.vhdl"
+        rtl_json: Optional[str] = None  # "rtl.json"
+        show_rtl: bool = False
+        show_rtl_flags: List[str] = [
+            "-stretch",
+            "-enum",
+            "-width",
+        ]
+        show_netlist: bool = False
+        show_netlist_flags: List[str] = ["-stretch", "-enum"]
+        ghdl: GhdlSynth.Settings = GhdlSynth.Settings()
+        post_synth_opt: bool = Field(
+            True,
+            description="run additional optimization steps after synthesis if complete",
+        )
+        verilog_lib: List[str] = []
+        splitnets: Optional[List[str]] = None  # ['-driver']
+        set_attributes: Dict[str, Dict[str, Any]] = {}
+        stop_after: Optional[Literal["rtl"]]
+
+        @validator("write_verilog_flags", pre=False)
+        def validate_write_verilog_flags(cls, value, values):  # type: ignore
+            if values.get("debug"):
+                value.append("-norename")
+            return value
+
+        @validator("verilog_lib", pre=True)
+        def validate_verilog_lib(cls, value, values):  # type: ignore
+            if isinstance(value, str):
+                value = [value]
+            value = [str(Path(v).resolve(strict=True)) for v in value]
+            return value
+
+        @validator("splitnets", pre=True)
+        def validate_splitnets(cls, value, values):  # type: ignore
+            if value is not None:
+                if isinstance(value, str):
+                    value = [value]
+                value = [
+                    v if not v.strip() or v.startswith("-") else f"-{v}" for v in value
+                ]
+            return value
+
+        @validator("set_attributes", pre=True, always=True)
+        def validate_set_attributes(cls, value, values):  # type: ignore
+            if value:
+                if isinstance(value, str):
+                    if value.endswith(".json"):
+                        attr_file = Path(value)
+                        try:
+                            log.info("Parsing %s as JSON file", attr_file)
+                            with open(attr_file) as f:
+                                value = {**json.load(f)}
+                        except json.JSONDecodeError as e:
+                            raise ValueError(
+                                f"Decoding of JSON file {attr_file} failed: {e.args}"
+                            ) from e
+                        except TypeError as e:
+                            raise ValueError(f"JSON TypeError: {e.args}") from e
+                    else:
+                        raise ValueError(
+                            f"Unsupported extension for JSON file: {value}"
+                        )
+                for attr, attr_dict in value.items():
+                    assert attr
+                    assert attr_dict, "attr_dict must be a non-empty Dict[str, Any]"
+                    for (path, v) in attr_dict.items():
+                        assert path and v
+                        if isinstance(path, list):
+                            path = "/".join(path)
+                        if isinstance(v, str):
+                            v = f'"{v}"'
+                        attr_dict[path] = v
+                    value[attr] = dict(attr_dict)
+            return value
+
+        @root_validator(pre=True)
+        def check_target(cls, values):  # type: ignore
+            assert values.get("fpga") or values.get(
+                "tech"
+            ), "ERROR in flows.yosys.settings: No targets specified! Either 'fpga' or 'tech' must be specified."
+            return values
+
+    def __init__(self, flow_settings: Settings, design: Design, run_path: Path):
+        super().__init__(flow_settings, design, run_path)
+        self.stat_report = "utilization.rpt"
+        self.timing_report = "timing.rpt"
+        self.artifacts = Box(
+            # non-plural names are preferred for keys, e.g. "digram" instead of "diagrams".
+            diagram={},
+            report=dict(utilization=self.stat_report, timing=self.timing_report),
+            rtl={},
+            netlist={},
+        )
+
+    def init(self) -> None:
+        assert isinstance(self.settings, self.Settings)
+
+        ss = self.settings
+        if ss.fpga:
+            assert ss.fpga.family or ss.fpga.vendor == "xilinx"
+        if ss.rtl_json:
+            self.artifacts.rtl.json = ss.rtl_json
+        if ss.rtl_vhdl:
+            self.artifacts.rtl.vhdl = ss.rtl_vhdl
+        if ss.rtl_verilog:
+            self.artifacts.rtl.verilog = ss.rtl_verilog
+        if not ss.stop_after:  # FIXME
+            if ss.netlist_json:
+                self.artifacts.netlist.json = ss.netlist_json
+            if ss.netlist_vhdl:
+                self.artifacts.netlist.vhdl = ss.netlist_vhdl
+            if ss.netlist_verilog:
+                self.artifacts.netlist.verilog = ss.netlist_verilog
+
+    def run(self) -> None:
+        assert isinstance(self.settings, self.Settings)
+        yosys = Tool(
+            executable="yosys",
+            docker=Docker(image="hdlc/impl"),  # pyright: reportGeneralTypeIssues=none
+        )
+        ss = self.settings
+        if ss.sta:
+            ss.flatten = True
+
+        if ss.flatten:
+            append_flag(ss.synth_flags, "-flatten")
+
+        # add FPGA-specific synth_xx flags
+        if ss.fpga:
+            if ss.abc9:  # ABC9 is for only FPGAs?
+                append_flag(ss.synth_flags, "-abc9")
+            if ss.retime:
+                append_flag(ss.synth_flags, "-retime")
+            if ss.dff:
+                append_flag(ss.synth_flags, "-dff")
+            if ss.nobram:
+                append_flag(ss.synth_flags, "-nobram")
+            if ss.nolutram:
+                append_flag(ss.synth_flags, "-nolutram")
+            if ss.nodsp:
+                append_flag(ss.synth_flags, "-nodsp")
+            if ss.nowidelut:
+                append_flag(ss.synth_flags, "-nowidelut")
+            if ss.widemux:
+                append_flag(ss.synth_flags, f"-widemux {ss.widemux}")
+            if ss.fpga.vendor == "xilinx":
+                ss.write_vhdl_flags.append("-unisim")
+        else:
+            if ss.dff:
+                append_flag(ss.abc_flags, "-dff")
+            if ss.tech:
+                if ss.tech.gates:
+                    append_flag(ss.abc_flags, f"-g {ss.tech.gates}")
+                elif ss.tech.liberty:
+                    liberty_path = Path(ss.tech.liberty)
+                    liberty_path = liberty_path.resolve().absolute()
+                    assert (
+                        liberty_path.exists()
+                    ), f"Specified tech.liberty={liberty_path} does not exist!"
+                    ss.tech.liberty = str(liberty_path)
+                    append_flag(ss.abc_flags, f"-liberty {ss.tech.liberty}")
+                elif ss.tech.lut:
+                    append_flag(ss.abc_flags, f"-lut {ss.tech.lut}")
+
+        script_path = self.copy_from_template(
+            "yosys.tcl",
+            ghdl_args=GhdlSynth.synth_args(ss.ghdl, self.design),
+        )
+        log.info(
+            "Yosys script: %s", self.run_path.relative_to(Path.cwd()) / script_path
+        )
+        # args = ['-s', script_path]
+        args = ["-c", script_path]
+        if ss.log_file:
+            args.extend(["-L", ss.log_file])
+        if not ss.verbose:  # reduce noise unless verbose
+            args.extend(["-T", "-Q", "-q"])
+        self.results["_tool"] = yosys.info  # TODO where should this go?
+        log.info("Logging yosys output to %s", ss.log_file)
+        yosys.run(*args)
+
+    def parse_reports(self) -> bool:
+        assert isinstance(self.settings, self.Settings)
+        if self.settings.fpga:
+            if self.settings.fpga.vendor == "xilinx":
+                self.parse_report_regex(
+                    self.artifacts.report.utilization,
+                    r"=== design hierarchy ===",
+                    r"FDRE\s*(?P<_FDRE>\d+)",
+                    r"FDSE\s*(?P<_FDSE>\d+)",
+                    r"number of LCs:\s*(?P<Estimated_LCs>\d+)",
+                    sequential=True,
+                    required=False,
+                )
+                self.results["FFs"] = int(self.results.get("_FDRE", 0)) + int(
+                    self.results.get("_FDSE", 0)
+                )
+            if self.settings.fpga.family == "ecp5":
+                self.parse_report_regex(
+                    self.artifacts.report.utilization,
+                    r"TRELLIS_FF\s+(?P<FFs>\d+)",
+                    r"LUT4\s+(?P<LUT4>\d+)",
+                )
+        return True
diff --git a/src/xeda/flows/yosys/templates/yosys.tcl b/src/xeda/flows/yosys/templates/yosys.tcl
new file mode 100644
index 00000000..9b6dc0ae
--- /dev/null
+++ b/src/xeda/flows/yosys/templates/yosys.tcl
@@ -0,0 +1,119 @@
+yosys -import
+set log_prefix "yosys> "
+{% for src in design.rtl.sources %}
+{%- if src.type == 'verilog' %}
+puts "$log_prefix Reading {{src}}"
+# -Dname=value -Idir
+read_verilog {{settings.read_verilog_flags|join(" ")}} {{src}}
+{%- elif src.type == 'systemverilog' %}
+puts "$log_prefix Reading {{src}}"
+read_verilog {{settings.read_verilog_flags|join(" ")}} -sv {{src}}
+{%- endif %}
+{%- endfor %}
+
+{%- set vhdl_files = design.rtl.sources | selectattr("type", "equalto", "vhdl") | list %}
+{%- if vhdl_files %}
+puts "$log_prefix Reading VHDL files: {{vhdl_files|join(" ")}}"
+yosys plugin -i ghdl
+yosys ghdl {{ghdl_args|join(" ")}}
+{%- endif %}
+{% if settings.tech and settings.tech.liberty -%}
+read_liberty {{settings.read_liberty_flags|join(" ")}} {{settings.tech.liberty}}
+{%- endif %}
+
+{% for src in settings.verilog_lib -%}
+read_verilog -lib {{src}}
+{%- endfor %}
+
+hierarchy -nodefaults -check {%- if design.rtl.top %} -top {{design.rtl.top}} {%- else %} -auto-top {%- endif %}
+
+check -initdrv -assert
+{%- for attr,attr_dict in settings.set_attributes.items() %}
+{%- for path,value in attr_dict.items() %}
+setattr -set {{attr}} {{value}} {{path}}
+{%- endfor %}
+{%- endfor %}
+
+{% if settings.rtl_json -%}
+prep {%- if settings.flatten %} -flatten {%- endif %} {%-if design.rtl.top %} -top {{design.rtl.top}} {% else %} -auto-top {%- endif %}
+puts "$log_prefix Writing JSON {{settings.rtl_json}}"
+write_json {{settings.rtl_json}}
+{%- endif %}
+{% if settings.rtl_verilog -%}
+puts "$log_prefix Writing Verilog {{settings.rtl_verilog}}"
+write_verilog {{settings.rtl_verilog}}
+{%- endif %}
+{% if settings.rtl_vhdl -%}
+puts "$log_prefix  Writing VHDL {{settings.rtl_vhdl}}"
+yosys write_vhdl {{settings.rtl_vhdl}}
+{%- endif %}
+{% if settings.show_rtl -%}
+show -prefix rtl_show -format dot {{settings.show_rtl_flags|join(" ")}}
+{%- endif %}
+
+{%if settings.stop_after != "rtl" -%}
+
+{%- if settings.fpga %}
+puts "$log_prefix Running FPGA synthesis for device {{settings.fpga}}"
+    {%- if settings.fpga.vendor == "xilinx" %}
+puts "$log_prefix Target: Xilinx {%if settings.fpga.part%} {{settings.fpga.part}} {%else%} {{settings.fpga.device}} {%endif%}"
+synth_xilinx {%- if settings.fpga.family %} -family {{settings.fpga.family}} {%- endif %} {{settings.synth_flags|join(" ")}}
+    {%- elif settings.fpga.family %}
+puts "$log_prefix  Target: {{settings.fpga.family}}"
+synth_{{settings.fpga.family}} {{settings.synth_flags|join(" ")}} {%- if design.rtl.top %} -top {{design.rtl.top}}{%- endif %}
+    {%- else %}
+puts "$log_prefix Unknown FPGA vendor, family, or device"
+    {%- endif %}
+{%- else %}
+puts "$log_prefix  Running synthesis"
+synth {{settings.synth_flags|join(" ")}} {%- if design.rtl.top %} -top {{design.rtl.top}}{%- endif %}
+    {%- if settings.tech %}
+        {% if settings.tech.liberty -%}
+puts "$log_prefix  Mapping FFs to technology library"
+dfflibmap -liberty {{settings.tech.liberty}}
+        {%- endif %}
+puts "$log_prefix  Running ABC"
+abc {{settings.abc_flags|join(" ")}}
+    {%- endif %}
+{%- endif %}
+
+{% if settings.post_synth_opt -%}
+puts "$log_prefix Final netlist optimization"
+opt -full -purge -sat
+opt -full -purge 
+{%- endif %}
+
+{% if settings.splitnets is not none -%}
+splitnets {{settings.splitnets|join(" ")}}
+{%- endif %}
+
+puts "$log_prefix Writing stat to {{artifacts["report"]["utilization"]}}"
+tee -q -o {{artifacts["report"]["utilization"]}} stat {%- if settings.fpga and settings.fpga.vendor == "xilinx" %} -tech xilinx {%-elif settings.tech and settings.tech.liberty%} -liberty {{settings.tech.liberty}}  {%- endif %}
+
+check {% if settings.check_assert -%} -assert {%- endif %}
+
+{% if settings.netlist_json -%}
+puts "$log_prefix Writing netlist {{settings.netlist_json}}"
+write_json {{settings.netlist_json}}
+{%- endif %}
+{% if settings.netlist_verilog -%}
+puts "$log_prefix Writing netlist {{settings.netlist_verilog}}"
+write_verilog {{settings.write_verilog_flags|join(" ")}} {{settings.netlist_verilog}}
+{%- endif %}
+{% if settings.netlist_vhdl -%}
+puts "$log_prefix Writing netlist {{settings.netlist_vhdl}}"
+yosys write_vhdl {{settings.write_vhdl_flags|join(" ")}} {{settings.netlist_vhdl}}
+{%- endif %}
+
+{% if settings.sta -%}
+puts "$log_prefix Writing timing report to {{artifacts["report"]["timing"]}}"
+tee -o {{artifacts["report"]["timing"]}} ltp
+tee -a {{artifacts["report"]["timing"]}} sta
+{%- endif %}
+
+{% if settings.show_netlist -%}
+puts "$log_prefix Writing netlist diagram to {{settings.show_netlist}}"
+show -prefix netlist_show -format dot {{settings.show_netlist_flags|join(" ")}}
+{%- endif %}
+
+{%- endif %}
\ No newline at end of file
diff --git a/src/xeda/flows/yosys/templates/yosys.ys b/src/xeda/flows/yosys/templates/yosys.ys
new file mode 100644
index 00000000..1314ff8f
--- /dev/null
+++ b/src/xeda/flows/yosys/templates/yosys.ys
@@ -0,0 +1,115 @@
+{% for src in design.rtl.sources %}
+{%- if src.type == 'verilog' %}
+log -stdout yosys> Reading {{src}}
+# -Dname=value -Idir
+read_verilog {{settings.read_verilog_flags|join(" ")}} {{src}}
+{%- elif src.type == 'systemverilog' %}
+log -stdout yosys> Reading {{src}}
+read_verilog {{settings.read_verilog_flags|join(" ")}} -sv {{src}}
+{%- endif %}
+{%- endfor %}
+
+{%- set vhdl_files = design.rtl.sources | selectattr("type", "equalto", "vhdl") | list %}
+{%- if vhdl_files %}
+log -stdout yosys> Compiling VHDL files
+plugin -i ghdl
+ghdl {{ghdl_args|join(" ")}}
+{%- endif %}
+{% if settings.tech and settings.tech.liberty -%}
+read_liberty {{settings.read_liberty_flags|join(" ")}} {{settings.tech.liberty}}
+{%- endif %}
+
+{% for src in settings.verilog_lib -%}
+read_verilog -lib {{src}}
+{%- endfor %}
+
+{%- if design.rtl.top %}
+hierarchy -check -top {{design.rtl.primary}}
+{%- else %}
+hierarchy -check -auto-top
+{%- endif %}
+check -initdrv -assert
+{%- for attr,attr_dict in settings.set_attributes.items() %}
+{%- for path,value in attr_dict.items() %}
+setattr -set {{attr}} {{value}} {{path}}
+{%- endfor %}
+{%- endfor %}
+
+{% if settings.rtl_json -%}
+log -stdout yosys> Writing JSON {{settings.rtl_json}}
+write_json {{settings.rtl_json}}
+{%- endif %}
+{% if settings.rtl_verilog -%}
+log -stdout yosys> Writing Verilog {{settings.rtl_verilog}}
+write_verilog {{settings.rtl_verilog}}
+{%- endif %}
+{% if settings.rtl_vhdl -%}
+log -stdout yosys> Writing VHDL {{settings.rtl_vhdl}}
+write_vhdl {{settings.rtl_vhdl}}
+{%- endif %}
+{% if settings.show_rtl -%}
+show -prefix rtl_show -format dot {{settings.show_rtl_flags|join(" ")}}
+{%- endif %}
+
+{%- if settings.fpga %}
+log -stdout yosys> Running FPGA synthesis for device {{settings.fpga}}
+    {%- if settings.fpga.vendor == "xilinx" %}
+log -stdout yosys> Target: Xilinx {%if settings.fpga.part%} {{settings.fpga.part}} {%else%} {{settings.fpga.device}} {%endif%}
+synth_xilinx {%- if settings.fpga.family %} -family {{settings.fpga.family}} {%- endif %} {{settings.synth_flags|join(" ")}}
+    {%- elif settings.fpga.family %}
+log -stdout yosys> Target: {{settings.fpga.family}}
+synth_{{settings.fpga.family}} {{settings.synth_flags|join(" ")}}
+    {%- else %}
+log -stderr yosys> Unknown FPGA vendor, family, or device
+    {%- endif %}
+{%- else %}
+log -stdout yosys> Running synthesis
+synth {{settings.synth_flags|join(" ")}}
+    {%- if settings.tech %}
+        {% if settings.tech.liberty -%}
+log -stdout yosys> Mapping FFs to lib
+dfflibmap -liberty {{settings.tech.liberty}}
+        {%- endif %}
+log -stdout yosys> Running ABC
+abc {{settings.abc_flags|join(" ")}}
+    {%- endif %}
+{%- endif %}
+
+{% if settings.post_synth_opt -%}
+log -stdout Post-synthesis optimization
+opt -full -purge -sat
+opt -full -purge 
+{%- endif %}
+
+{% if settings.splitnets is not none -%}
+splitnets {{settings.splitnets|join(" ")}}
+{%- endif %}
+
+log -stdout yosys> Writing stat
+tee -q -o {{artifacts.report.utilization}} stat {%- if settings.fpga and settings.fpga.vendor == "xilinx" %} -tech xilinx {%-elif settings.tech and settings.tech.liberty%} -liberty {{settings.tech.liberty}}  {%- endif %}
+
+check {% if settings.check_assert -%} -assert {%- endif %}
+
+{% if settings.netlist_json -%}
+log -stdout yosys> Writing netlist {{settings.netlist_json}}
+write_json {{settings.netlist_json}}
+{%- endif %}
+{% if settings.netlist_verilog -%}
+log -stdout yosys> Writing netlist {{settings.netlist_verilog}}
+write_verilog {{settings.write_verilog_flags|join(" ")}} {{settings.netlist_verilog}}
+{%- endif %}
+{% if settings.netlist_vhdl -%}
+log -stdout yosys> Writing netlist {{settings.netlist_vhdl}}
+write_vhdl {{settings.write_vhdl_flags|join(" ")}} {{settings.netlist_vhdl}}
+{%- endif %}
+
+{% if settings.sta -%}
+log -stdout yosys> Writing timing report to {{artifacts.report.timing}}
+tee -o {{artifacts.report.timing}} ltp
+tee -a {{artifacts.report.timing}} sta
+{%- endif %}
+
+{% if settings.show_netlist -%}
+log -stdout yosys> Writing netlist diagram to netlist_show.dot
+show -prefix netlist_show -format dot {{settings.show_netlist_flags|join(" ")}}
+{%- endif %}
\ No newline at end of file
diff --git a/src/xeda/fpga.py b/src/xeda/fpga.py
new file mode 100644
index 00000000..8ef600c3
--- /dev/null
+++ b/src/xeda/fpga.py
@@ -0,0 +1,158 @@
+import re
+import logging
+from typing import Any, Optional
+
+from .dataclass import Field, XedaBaseModel, root_validator
+
+log = logging.getLogger(__name__)
+
+
+__all__ = [  #
+    "FPGA",  #
+]
+
+
+class FPGA(XedaBaseModel):
+    """FPGA target device"""
+
+    # definition order: part > device > vendor > {family, speed, package, etc}
+    part: Optional[str] = Field(None, description="full device part identifier")
+    device: Optional[str]
+    vendor: Optional[str]
+    family: Optional[str]
+    generation: Optional[str]
+    type: Optional[str]
+    speed: Optional[str] = Field(None, description="speed-grade")
+    package: Optional[str]
+    capacity: Optional[str]
+    pins: Optional[int]
+    grade: Optional[str]
+
+    def __init__(self, *args: str, **data: Any) -> None:
+        if args:
+            if len(args) != 1 or not args[0]:
+                raise ValueError("Only a single 'part' non-keyword argument is valid.")
+            a = args[0]
+            if isinstance(a, str):
+                if "part" in data:
+                    raise ValueError("'part' field already given in keyword arguments.")
+                data["part"] = a
+            elif isinstance(a, dict):
+                if data:
+                    raise ValueError("Both dictionary and keyword arguments preset")
+                data = a
+            else:
+                raise ValueError(f"Argument of type {type(a)} is not supported!")
+        log.debug("fpga init! data=%s", data)
+        super().__init__(**data)
+
+    # this is called before all field validators!
+    @root_validator(pre=True)
+    def _fpga_root_validator(cls, values):  # pylint: disable=no-self-argument
+        if not values:
+            return values
+        # Intel: https://www.intel.com/content/dam/www/central-libraries/us/en/documents/product-catalog.pdf
+        # Lattice: https://www.latticesemi.com/Support/PartNumberReferenceGuide
+        # Xilinx: https://www.xilinx.com/support/documents/selection-guides/7-series-product-selection-guide.pdf
+        #         https://docs.xilinx.com/v/u/en-US/ds890-ultrascale-overview
+        #         https://www.xilinx.com/support/documents/selection-guides/ultrascale-fpga-product-selection-guide.pdf
+        #         https://www.xilinx.com/support/documents/selection-guides/ultrascale-plus-fpga-product-selection-guide.pdf
+        part = values.get("part")
+
+        def set_if_not_exist(attr: str, v: Any) -> None:
+            if attr not in values:
+                values[attr] = v
+
+        def set_xc_family(s: str):
+            d = dict(s="spartan", a="artix", k="kintex", v="virtex", z="zynq")
+            s = s.lower()
+            if s in d:
+                set_if_not_exist("family", d[s])
+
+        if part:
+            part = part.strip()
+            values["part"] = part
+            # speed: 6 = slowest, 8 = fastest
+            match_ecp5 = re.match(
+                r"^LFE5(U|UM|UM5G)-(\d+)F-(?P<sp>\d)(?P<pkg>[A-Z]+)(?P<pin>\d+)(?P<gr>[A-Z]?)$",
+                part,
+                flags=re.IGNORECASE,
+            )
+            if match_ecp5:
+                set_if_not_exist("vendor", "lattice")
+                set_if_not_exist("family", "ecp5")
+                set_if_not_exist("type", match_ecp5.group(1).lower())
+                set_if_not_exist("device", "LFE5" + match_ecp5.group(1).upper())
+                set_if_not_exist("capacity", match_ecp5.group(2) + "k")
+                set_if_not_exist("speed", match_ecp5.group("sp"))
+                set_if_not_exist("package", match_ecp5.group("pkg"))
+                set_if_not_exist("pins", int(match_ecp5.group("pin")))
+                set_if_not_exist("grade", match_ecp5.group("gr"))
+                return values
+            # Commercial Xilinx # Generation # Family # Logic Cells in 1K units # Speed Grade (-1 slowest, L: low-power) # Package Type
+            match_xc7 = re.match(
+                r"^(XC)(?P<g>\d)(?P<f>[A-Z])(?P<lc>\d+)-(?P
-L?\d)(?P<pkg>[A-Z][A-Z][A-Z]+)(?P<pin>\d\d+)(?P<gr>[A-Z]?)$",
+                part,
+                flags=re.IGNORECASE,
+            )
+            if match_xc7:
+                set_if_not_exist("vendor", "xilinx")
+                set_if_not_exist("generation", match_xc7.group("g"))
+                set_xc_family(match_xc7.group("f"))
+                lc = match_xc7.group("lc")
+                set_if_not_exist(
+                    "device",
+                    match_xc7.group(1)
+                    + match_xc7.group("g")
+                    + match_xc7.group("f")
+                    + lc,
+                )
+                set_if_not_exist("capacity", lc + "K")
+                set_if_not_exist("package", int(match_xc7.group("pkg")))
+                set_if_not_exist("pins", int(match_xc7.group("pins")))
+                set_if_not_exist("grade", match_xc7.group("gr"))
+                return values
+            match_us = re.match(
+                r"^(XC)(?P<f>[A-Z])(?P<g>[A-Z]+)(?P<lc>\d+)-(?P
-L?\d)(?P<pkg>[A-Z][A-Z][A-Z]+)(?P<pin>\d\d+)(?P<gr>[A-Z]?)$",
+                part,
+                flags=re.IGNORECASE,
+            )
+            if match_us:
+                set_if_not_exist("vendor", "xilinx")
+                set_if_not_exist("generation", match_us.group("g"))
+                set_xc_family(match_us.group("f"))
+                lc = match_us.group("lc")
+                set_if_not_exist(
+                    "device",
+                    match_us.group(1) + match_us.group("g") + match_us.group("f") + lc,
+                )
+                set_if_not_exist("capacity", lc + "K")
+                set_if_not_exist("package", int(match_us.group("pkg")))
+                set_if_not_exist("pins", int(match_us.group("pins")))
+                set_if_not_exist("grade", match_us.group("gr"))
+                return values
+            # UltraSCALE+
+            # capacity is index to table, roughly x100K LCs
+            match_usp = re.match(
+                r"^(XC)(?P<f>[A-Z])U(?P<lc>\d+)P-(?P
-L?\d)(?P<pkg>[A-Z][A-Z][A-Z]+)(?P<pin>\d\d+)(?P<gr>[A-Z]?)$",
+                part,
+                flags=re.IGNORECASE,
+            )
+            if match_usp:
+                set_if_not_exist("vendor", "xilinx")
+                set_if_not_exist("generation", "usp")
+                set_xc_family(match_usp.group("f"))
+                lc = match_usp.group("lc")
+                set_if_not_exist("capacity", lc)
+                set_if_not_exist(
+                    "device", match_usp.group(1) + match_usp.group("f") + "U" + lc + "P"
+                )
+                set_if_not_exist("package", int(match_usp.group("pkg")))
+                set_if_not_exist("pins", int(match_usp.group("pins")))
+                set_if_not_exist("grade", match_usp.group("gr"))
+                return values
+        elif not values.get("device") and not values.get("vendor"):
+            raise ValueError(
+                "Missing enough information about the FPGA device. Please set the 'part' number and/or device, vendor, family, etc."
+            )
+        return values
diff --git a/src/xeda/gtkwave.py b/src/xeda/gtkwave.py
new file mode 100644
index 00000000..569c1ef6
--- /dev/null
+++ b/src/xeda/gtkwave.py
@@ -0,0 +1,35 @@
+import logging
+import os
+from pathlib import Path
+from typing import Union
+
+from vcd import gtkw
+
+log = logging.getLogger(__name__)
+
+
+def _add_sig(g: gtkw.GTKWSave, root_group, signals):
+    i = 0
+    while i < len(signals):
+        sig = signals[i]
+        if sig[: len(root_group)] == root_group:  # in this group
+            if len(sig) - len(root_group) > 1:
+                new_group = sig[len(root_group)]
+                with g.group(new_group, closed=True):
+                    i += _add_sig(g, root_group + [new_group], signals[i:])
+            else:
+                g.trace(".".join(sig))
+                i += 1
+        else:
+            break
+    return i
+
+
+def gen_gtkw(dump_file: Union[str, os.PathLike], signals, root_group):
+    root_group = root_group or []
+    save_file = Path(dump_file).with_suffix(".gtkw")
+    log.info("generated gtkwave save file: %s", save_file.absolute())
+    with open(save_file, "w") as f:
+        g = gtkw.GTKWSave(f)
+        g.dumpfile(str(dump_file), abspath=not os.path.isabs(dump_file))
+        _add_sig(g, root_group, signals)
diff --git a/src/xeda/tool.py b/src/xeda/tool.py
new file mode 100644
index 00000000..7223cb74
--- /dev/null
+++ b/src/xeda/tool.py
@@ -0,0 +1,480 @@
+import contextlib
+import logging
+import os
+import re
+import subprocess
+import sys
+from pathlib import Path
+from sys import stderr
+from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
+
+from .dataclass import Field, XedaBaseModeAllowExtra, XedaBaseModel, validator
+from .utils import cached_property, unique
+
+log = logging.getLogger(__name__)
+
+
+__all__ = [
+    "ToolException",
+    "NonZeroExitCode",
+    "ExecutableNotFound",
+    "Docker",
+    "Tool",
+    "run_process",
+]
+
+
+class ToolException(Exception):
+    """Super-class of all tool exceptions"""
+
+
+class NonZeroExitCode(ToolException):
+    def __init__(self, command_args: Any, exit_code: int, *args: object) -> None:
+        self.command_args = command_args
+        self.exit_code = exit_code
+        super().__init__(*args)
+
+
+class ExecutableNotFound(ToolException):
+    def __init__(
+        self, executable: str, tool: str, path: str, *args: Any, **kwargs: Any
+    ) -> None:
+        super().__init__(*args, **kwargs)
+        self.exec = executable
+        self.tool = tool
+        self.path = path
+
+
+class RemoteSettings(XedaBaseModel):
+    enabled: bool = False
+    junest_path: str  # FIXME REMOVE
+    junest_method: str = "ns"
+    junest_mounts: Dict[str, str] = {}
+    exec_path: Optional[str] = None
+    hostname: str
+    port: int = 22
+
+
+OptionalPath = Union[None, str, os.PathLike]
+OptionalBoolOrPath = Union[None, bool, str, os.PathLike]
+
+
+class Docker(XedaBaseModel):
+    enabled: bool = False
+    command: List[str] = []
+    platform: Optional[str]
+    image: str = Field(description="Docker image name")
+    tag: str = Field("latest", description="Docker image tag")
+    registry: Optional[str] = Field(None, description="Docker image registry")
+    mounts: Dict[str, str] = {}
+
+    # TODO this is only for a Linux container
+    @cached_property
+    def _cpuinfo(self) -> List[List[str]]:
+        ret = self._run_docker("cat", "/proc/cpuinfo", stdout=True)
+        assert ret
+        return [x.split("\n") for x in re.split(r"\n\s*\n", ret, re.MULTILINE)]
+
+    @property
+    def cpuinfo(self) -> List[List[str]]:
+        return self._cpuinfo
+
+    @property
+    def nproc(self) -> Optional[int]:
+        return len(self._cpuinfo)
+
+    @property
+    def name(self) -> str:
+        return self.command[0] if self.command else "_"
+
+    def run(
+        self,
+        *args: Any,
+        env: Optional[Dict[str, Any]] = None,
+        stdout: OptionalBoolOrPath = None,
+        check: bool = True,
+        root_dir: OptionalPath = None,
+    ) -> Union[None, str]:
+        """Run the tool from a docker container"""
+        cpuinfo_file = Path(".cpuinfo").resolve()
+        with open(cpuinfo_file, "w") as f:
+            for proc in self.cpuinfo:
+                for line in proc:
+                    assert isinstance(line, str)
+                    if line.startswith("Features"):
+                        line += " sse sse2"
+                    f.write(line + "\n")
+                f.write("\n")
+
+        self.mounts[str(cpuinfo_file)] = "/proc/cpuinfo"
+        return self._run_docker(
+            *self.command, *args, env=env, stdout=stdout, check=check, root_dir=root_dir
+        )
+
+    def _run_docker(
+        self,
+        *args: Any,
+        env: Optional[Dict[str, Any]] = None,
+        stdout: OptionalBoolOrPath = None,
+        check: bool = True,
+        root_dir: OptionalPath = None,
+    ) -> Union[None, str]:
+        cwd = Path.cwd()
+        wd = root_dir if root_dir else cwd
+        if not isinstance(wd, Path):
+            wd = Path(wd)
+        docker_args = [
+            "--rm",
+            f"--workdir={wd}",
+        ]
+        self.mounts[str(wd)] = str(wd)
+        self.mounts[str(cwd)] = str(cwd)
+        if not stdout and sys.stdout.isatty():
+            docker_args += ["--tty", "--interactive"]
+        if self.platform:
+            docker_args += ["--platform", self.platform]
+        for k, v in self.mounts.items():
+            docker_args.append(f"--volume={k}:{v}")
+        if env:
+            env_file = wd / f".{self.name}_docker.env"
+            with open(env_file, "w") as f:
+                f.write("\n".join(f"{k}={v}" for k, v in env.items()))
+            docker_args.extend(["--env-file", str(env_file)])
+
+        return run_process(
+            "docker",
+            ["run", *docker_args, f"{self.image}:{self.tag}", *args],
+            env=None,
+            stdout=stdout,
+            check=check,
+            tool_name=self.name,
+        )
+
+
+def run_process(
+    executable: str,
+    args: Optional[Sequence[Any]] = None,
+    env: Optional[Dict[str, Any]] = None,
+    stdout: OptionalBoolOrPath = None,
+    check: bool = True,
+    cwd: OptionalPath = None,
+    tool_name: str = "",
+) -> Union[None, str]:
+    if args is None:
+        args = []
+    args = [str(a) for a in args]
+    if env is not None:
+        env = {k: str(v) for k, v in env.items()}
+    log.info("Running `%s`", " ".join([executable, *args]))
+    if cwd:
+        log.info("cwd=%s", cwd)
+    if stdout and isinstance(stdout, (str, os.PathLike)):
+        stdout = Path(stdout)
+        log.info("redirecting stdout to %s", stdout)
+
+        def cm_call():
+            assert stdout
+            return open(stdout, "w")
+
+        cm = cm_call
+    else:
+        cm = contextlib.nullcontext
+    with cm() as f:
+        try:
+            with subprocess.Popen(
+                [executable, *args],
+                cwd=cwd,
+                shell=False,
+                stdout=f if f else subprocess.PIPE if stdout else None,
+                bufsize=1,
+                universal_newlines=True,
+                encoding="utf-8",
+                errors="replace",
+                env=env,
+            ) as proc:
+                log.info("Started %s[%d]", executable, proc.pid)
+                if stdout:
+                    if isinstance(stdout, bool):
+                        out, err = proc.communicate(timeout=None)
+                        if check and proc.returncode != 0:
+                            raise NonZeroExitCode(proc.args, proc.returncode)
+                        if err:
+                            print(err, file=stderr)
+                        return out.strip()
+                    log.info("Standard output is logged to: %s", stdout)
+                else:
+                    proc.wait()
+        except FileNotFoundError as e:
+            path = env["PATH"] if env and "PATH" in env else os.environ.get("PATH", "")
+            raise ExecutableNotFound(e.filename, tool_name, path, *e.args) from None
+    if check and proc.returncode != 0:
+        raise NonZeroExitCode(proc.args, proc.returncode)
+    return None
+
+
+def fake_cpu_info(file=".xeda_cpuinfo", ncores=4):
+    with open(file, "w") as f:
+        for i in range(ncores):
+            cpuinfo: Dict[str, Any] = {
+                "processor": i,
+                "vendor_id": "GenuineIntel",
+                "family": 6,
+                "model": 60,
+                "core id": i,
+                "cpu cores": ncores,
+                "fpu": "yes",
+                "model name": "Intel(R) Core(TM) i7-4790K CPU @ 4.00GHz",
+                "flags": "fpu vme tsc msr pae mce cx8 mmx fxsr sse sse2 avx2",
+            }
+            for k, v in cpuinfo.items():
+                ws = "\\t" * (1 if len(k) >= 8 else 2)
+                f.write(f"{k}{ws}: {v}")
+
+
+def _run_processes(commands: List[List[str]], cwd: OptionalPath = None) -> None:
+    """Run a list commands to completion. Throws if any of them did not execute and exit normally"""
+    # if args:
+    #     args = [str(a) for a in args]
+    # if env:
+    #     env = {str(k): str(v) for k, v in env.items()}
+    processes = []
+    for cmd in commands:
+        log.info("Running `%s`", " ".join(cmd))
+        with subprocess.Popen(
+            cmd,
+            cwd=cwd,
+            shell=False,
+            #   stdout=subprocess.PIPE if stdout else None,
+            bufsize=1,
+            universal_newlines=True,
+            encoding="utf-8",
+            errors="replace",
+            #   env=env
+        ) as proc:
+            assert isinstance(proc.args, list)
+            log.info("Started %s[%d]", str(proc.args[0]), proc.pid)
+            processes.append(proc)
+
+    for p in processes:
+        p.wait()
+
+    for p in processes:
+        if p.returncode != 0:
+            raise Exception(f"Process exited with return code {p.returncode}")
+
+
+class Tool(XedaBaseModeAllowExtra):
+    """abstraction for an EDA tool"""
+
+    executable: str
+    minimum_version: Union[None, Tuple[Union[int, str], ...]] = None
+    default_args: Optional[List[str]] = None
+
+    remote: Optional[RemoteSettings] = Field(None, hidden_from_schema=True)
+    docker: Optional[Docker] = Field(None, hidden_from_schema=True)
+    log_stdout: bool = Field(
+        False, description="Log stdout to a file", hidden_from_schema=True
+    )
+    log_stderr: bool = Field(
+        False, description="Log stderr to a file", hidden_from_schema=True
+    )
+    bin_path: str = Field(
+        None, description="Path to the tool binary", hidden_from_schema=True
+    )
+
+    def __init__(self, executable: Optional[str] = None, **kwargs):
+        if executable:
+            assert "executable" not in kwargs, "executable specified twice"
+            kwargs["executable"] = executable
+        super().__init__(**kwargs)
+
+    @validator("docker", pre=True, always=True)
+    def validate_docker(cls, value, values):
+        if value and not value.command:
+            value.command = [values.get("executable")]
+        return value
+
+    @cached_property
+    def _info(self) -> Dict[str, str]:
+        log.warning("Tool.info is not implemented for %s", self.__class__.__name__)
+        return {}
+
+    @property  # pydantic can't handle cached_property as private
+    def info(self) -> Dict[str, str]:
+        inf = self._info
+        inf["version"] = ".".join(self.version)
+        return inf
+
+    @cached_property
+    def _version(self) -> Tuple[str, ...]:
+        out = self.run_get_stdout(
+            "--version",
+        )
+        assert isinstance(out, str)
+        so = re.split(r"\s+", out)
+        version_string = so[1] if len(so) > 1 else so[0] if len(so) > 0 else ""
+        return tuple(version_string.split("."))
+
+    @property
+    def version(self) -> Tuple[str, ...]:
+        return self._version
+
+    @staticmethod
+    def _version_is_gte(v1: Tuple[str, ...], v2: Tuple[Union[int, str], ...]) -> bool:
+        """version v1 is greater than or equal to v2"""
+        for tv_s, sv_s in zip(v2, v1):
+            try:
+                tv = int(tv_s)
+            except ValueError:
+                tv = -1
+            try:
+                sv = int(sv_s)
+            except ValueError:
+                sv = -1
+            if sv < tv:
+                return False
+            if sv > tv:
+                return True
+        return True
+
+    def version_gte(self, *args: Union[int, str]) -> bool:
+        """Tool version is greater than or equal to version specified in args"""
+        return self._version_is_gte(self.version, args)
+
+    def _run_process(
+        self,
+        args: Optional[Sequence[Any]] = None,
+        env: Optional[Dict[str, Any]] = None,
+        stdout: OptionalBoolOrPath = None,
+        check: bool = True,
+        cwd: OptionalPath = None,
+    ):
+        return run_process(
+            self.executable,
+            args,
+            env=env,
+            stdout=stdout,
+            check=check,
+            cwd=cwd,
+            tool_name=self.__class__.__name__,
+        )
+
+    def _run_system(
+        self,
+        *args: Any,
+        stdout: OptionalBoolOrPath = None,
+        env: Optional[Dict[str, Any]] = None,
+        cwd: OptionalPath = None,
+        check: bool = True,
+    ) -> Union[None, str]:
+        """Run the tool if locally installed on the system and available on the current user's PATH"""
+        if env is not None:
+            env = {**os.environ, **env}
+        return self._run_process(args, env=env, stdout=stdout, check=check, cwd=cwd)
+
+    def _run_remote(
+        self,
+        *args: Any,
+        env: Optional[Dict[str, Any]] = None,
+        stdout: OptionalBoolOrPath = None,
+        check: bool = True,
+        cwd: OptionalPath = None,
+    ) -> None:
+        # FIXME remote execution is broken
+        #
+        # if env is not None:
+        # env = {**os.environ, **env}
+        assert self.remote, "tool.remote settings not available!"
+
+        remote_sshfs_mount_dir = "~/mnt"
+        wd = str(cwd) if cwd else os.curdir
+        self.remote.junest_mounts[remote_sshfs_mount_dir] = wd
+        mount_opts = [f"--bind {k} {v}" for k, v in self.remote.junest_mounts.items()]
+        junest_backend_opts = ["-b"] + mount_opts
+        executable = self.executable
+        if self.remote.exec_path:
+            executable = os.path.join(self.remote.exec_path, executable)
+        junest_cmd = (
+            [self.remote.junest_path]
+            + junest_backend_opts
+            + ["--", executable]
+            + list(*args)
+        )
+
+        remote_cmd = junest_cmd
+        client_nc_port = 34567
+        reverse_sftp_port = 10000
+
+        sshfs_opts = ["directport=10000", "idmap=user", "exec", "compression=yes"]
+
+        sshfs_cmd = [
+            "sshfs",
+            "-o",
+            ",".join(sshfs_opts),
+            f"localhost:{wd}",
+            remote_sshfs_mount_dir,
+        ]
+
+        ssh_proc = [
+            "ssh",
+            self.remote.hostname,
+            "-p",
+            str(self.remote.port),
+        ]
+
+        sshfs_proc = (
+            ssh_proc
+            + [
+                "-R",
+                f"{reverse_sftp_port}:localhost:{client_nc_port}",
+                "mkdir",
+                "-p",
+                remote_sshfs_mount_dir,
+                "&&",
+            ]
+            + sshfs_cmd
+            + ["&&"]
+            + remote_cmd
+        )
+
+        ncat_proc = [
+            "ncat",
+            "-l",
+            "-p",
+            f"{client_nc_port}",
+            "-e",
+            "/usr/libexec/sftp-server",
+        ]
+        _run_processes([sshfs_proc, ncat_proc])
+
+    def _run(
+        self,
+        *args: Any,
+        env: Optional[Dict[str, Any]] = None,
+        stdout: OptionalBoolOrPath = None,
+        check: bool = True,
+    ) -> Union[None, str]:
+        if self.default_args:
+            args = tuple(unique(self.default_args + list(args)))
+        if self.remote and self.remote.enabled:
+            self._run_remote(*args, env=env, stdout=stdout, check=check)
+            return None
+        if self.docker and self.docker.enabled:
+            return self.docker.run(*args, env=env, stdout=stdout, check=check)
+        return self._run_system(*args, env=env, stdout=stdout, check=check)
+
+    def run(self, *args: Any, env: Optional[Dict[str, Any]] = None) -> None:
+        self._run(*args, env=env, stdout=None)
+
+    def run_get_stdout(self, *args: Any, env: Optional[Dict[str, Any]] = None) -> str:
+        out = self._run(*args, env=env, stdout=True)
+        assert isinstance(out, str)
+        return out
+
+    def run_stdout_to_file(
+        self,
+        *args: Any,
+        stdout: OptionalBoolOrPath,
+        env: Optional[Dict[str, Any]] = None,
+    ) -> None:
+        self._run(*args, env=env, stdout=stdout)
diff --git a/src/xeda/types.py b/src/xeda/types.py
new file mode 100644
index 00000000..4f045bef
--- /dev/null
+++ b/src/xeda/types.py
@@ -0,0 +1,6 @@
+"""Type definitions"""
+import os
+from pathlib import Path
+from typing import Union
+
+PathLike = Union[str, Path, os.PathLike]
diff --git a/src/xeda/utils.py b/src/xeda/utils.py
new file mode 100644
index 00000000..28a4b6b4
--- /dev/null
+++ b/src/xeda/utils.py
@@ -0,0 +1,311 @@
+"utility functions and classes"
+import importlib
+import json
+import logging
+import os
+import re
+from contextlib import AbstractContextManager
+from copy import deepcopy
+from datetime import datetime
+from functools import cached_property, reduce
+from pathlib import Path
+from types import TracebackType
+from typing import (
+    Any,
+    Dict,
+    Iterable,
+    List,
+    Optional,
+    OrderedDict,
+    Tuple,
+    Type,
+    TypeVar,
+    Union,
+)
+
+from typeguard.importhook import install_import_hook
+
+
+from varname import argname
+
+from .dataclass import XedaBaseModel
+
+try:
+    import tomllib  # type: ignore # pyright: reportMissingImports=none
+except ModuleNotFoundError:
+    # python_version < "3.11":
+    import tomli as tomllib  # type: ignore
+
+
+install_import_hook("xeda")
+
+__all__ = [
+    "SDF",
+    # utility functions
+    "toml_load",
+    "toml_loads",
+    "cached_property",
+    "unique",
+    "WorkingDirectory",
+]
+
+log = logging.getLogger(__name__)
+
+
+T = TypeVar("T")
+
+
+class WorkingDirectory(AbstractContextManager):
+    def __init__(self, wd: Union[None, str, os.PathLike]):
+        self.prev_wd: Optional[Path] = None
+        if wd is not None and not isinstance(wd, Path):
+            wd = Path(wd)
+        self.wd = wd
+
+    def __enter__(self) -> None:
+        self.prev_wd = Path.cwd()
+        if self.wd:
+            log.debug("Changing working directory from %s to %s", self.prev_wd, self.wd)
+            os.chdir(self.wd)
+
+    def __exit__(
+        self,
+        exception_type: Optional[Type[BaseException]],
+        exception_value: Optional[BaseException],
+        exception_traceback: Optional[TracebackType],
+    ) -> None:
+        if self.prev_wd:
+            log.debug("Changing back working directory to %s", self.prev_wd)
+            os.chdir(self.prev_wd)
+
+
+class SDF(XedaBaseModel):
+    """
+    root: region where the SDF is applied
+        /TESTBENCH/UUT
+    if tb.top=TESTBENCH and the simulation netlist is instantiated inside the testbench with an instance name of UUT
+    """
+
+    root: Optional[str] = None
+    min: Optional[str] = None
+    max: Optional[str] = None
+    typ: Optional[str] = None
+
+    def __init__(self, *args: str, **data: str) -> None:
+        if args:
+            assert len(args) == 1 and isinstance(args[0], str), "only 1 str argument"
+            data["max"] = args[0]
+        super().__init__(**data)
+
+    def delay_items(self) -> Iterable[Tuple[str, Union[str, None]]]:
+        """returns an iterable of (delay_type, sdf_file)"""
+        return tuple(
+            (delay_type, self.dict().get(delay_type))
+            for delay_type in ("min", "max", "typ")
+            if self.dict().get(delay_type)
+        )
+
+
+def toml_load(path: Union[str, os.PathLike]) -> Dict[str, Any]:  # type: ignore
+    with open(path, "rb") as f:
+        return tomllib.load(f)  # type: ignore
+
+
+def toml_loads(s: str) -> Dict[str, Any]:
+    return tomllib.loads(s)  # type: ignore
+
+
+def backup_existing(path: Path) -> Optional[Path]:
+    if not path.exists():
+        log.warning("%s does not exist for backup!", path)
+        return None
+    modifiedTime = os.path.getmtime(path)
+    suffix = (
+        f'.backup_{datetime.fromtimestamp(modifiedTime).strftime("%Y-%m-%d-%H%M%S")}'
+    )
+    if path.suffix:
+        suffix += path.suffix
+    backup_path = path.with_suffix(suffix)
+    typ = "file" if path.is_file() else "directory" if path.is_dir() else "???"
+    log.warning(
+        "Renaming existing %s from '%s' to '%s'", typ, path.name, backup_path.name
+    )
+    # TODO use shutil.move instead? os.rename vs Path.rename?
+    # os.rename(path, backup_path)
+    return path.rename(backup_path)
+
+
+def dump_json(data: object, path: Path, backup_previous: bool = True) -> None:
+    if path.exists() and backup_previous:
+        backup_existing(path)
+        assert not path.exists(), "Old file still exists!"
+
+    with open(path, "w") as outfile:
+        json.dump(
+            data,
+            outfile,
+            default=lambda x: x.__dict__ if hasattr(x, "__dict__") else str(x),
+            indent=4,
+        )
+
+
+def unique(lst: List[Any]) -> List[Any]:
+    """uniquify list while preserving order"""
+    return list(OrderedDict.fromkeys(lst))
+
+
+def camelcase_to_snakecase(name: str) -> str:
+    name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
+    return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower()
+
+
+def snakecase_to_camelcase(name: str) -> str:
+    return "".join(word.title() for word in name.split("_"))
+
+
+def load_class(
+    full_class_string: str, defualt_module_name: Optional[str] = None
+) -> Optional[type]:
+    cls_path_lst = full_class_string.split(".")
+    assert len(cls_path_lst) > 0
+
+    cls_name = snakecase_to_camelcase(cls_path_lst[-1])
+    if len(cls_path_lst) == 1:  # module name not specified, use default
+        mod_name = defualt_module_name
+    else:
+        mod_name = ".".join(cls_path_lst[:-1])
+    assert mod_name
+
+    module = importlib.import_module(
+        mod_name, __package__ if mod_name.startswith(".") else None
+    )
+    cls = getattr(module, cls_name)
+    if not isinstance(cls, type):
+        return None
+    return cls
+
+
+def dict_merge(
+    base_dict: Dict[Any, Any], merge_dict: Dict[Any, Any], add_new_keys: bool = True
+) -> Dict[Any, Any]:
+    """
+    returns content of base_dict merge with content of merge_dict.
+    if add_new_keys=False keys in merge_dict not existing in base_dict are ignored
+    """
+    rtn_dct = deepcopy(base_dict)
+    if add_new_keys is False:
+        merge_dict = {
+            key: merge_dict[key] for key in set(rtn_dct).intersection(set(merge_dict))
+        }
+
+    rtn_dct.update(
+        {
+            key: dict_merge(rtn_dct[key], merge_dict[key], add_new_keys=add_new_keys)
+            if isinstance(rtn_dct.get(key), dict) and isinstance(merge_dict[key], dict)
+            else merge_dict[key]
+            for key in merge_dict
+        }
+    )
+    return rtn_dct
+
+
+def try_convert(
+    s: Any, convert_lists: bool = False
+) -> Union[bool, int, float, str, List[Union[bool, int, float, str, List[Any]]]]:
+    if s is None:
+        return "None"
+    if isinstance(s, str):
+        s = s.strip()
+        assert isinstance(s, str)
+        if s.startswith('"') or s.startswith("'"):
+            return s.strip("\"'")
+        if convert_lists and s.startswith("[") and s.endswith("]"):
+            s = re.sub(r"\s+", "", s)
+            return try_convert(list(s.strip("][").split(",")))
+        # Should NOT convert dict, set, etc!
+        if re.match(r"^\d+$", s):
+            return int(s)
+        if s.lower() in ["true", "yes"]:
+            return True
+        if s.lower() in ["false", "no"]:
+            return False
+        try:
+            return float(s)
+        except ValueError:
+            pass
+        return s
+    if isinstance(s, (int, float, bool)):
+        return s
+    if isinstance(s, (tuple)):
+        s = list(s)
+    if isinstance(s, (list)):
+        return [try_convert(e) for e in s]
+    return str(s)
+
+
+def get_hierarchy(dct: Dict[str, Any], path, sep="."):
+    if isinstance(path, str):
+        path = path.split(sep)
+    try:
+        return reduce(dict.__getitem__, path, dct)
+    except ValueError:
+        return None
+
+
+def set_hierarchy(dct: Dict[str, Any], path, value, sep="."):
+    if isinstance(path, str):
+        path = path.split(sep)
+    k = path[0]
+    if len(path) == 1:
+        dct[k] = value
+    else:
+        if k not in dct:
+            dct[k] = {}
+        set_hierarchy(dct[k], path[1:], value, sep)
+
+
+def append_flag(flag_list: List[str], flag: str) -> List[str]:
+    if flag not in flag_list:
+        flag_list.append(flag)
+    return flag_list
+
+
+def common_root(signals: List[List[T]]) -> List[T]:
+    longest: Optional[List[T]] = None
+    for sig in signals:
+        if not sig:
+            continue
+        if longest is None:
+            longest = sig[:-1]
+            continue
+        new_len = min(len(longest), len(sig))
+        longest = longest[:new_len]
+        if not longest:
+            break
+        for i in range(new_len):
+            if sig[i] != longest[i]:
+                longest = longest[:i]
+                break
+    return longest if longest else []
+
+
+def setting_flag(variable: Any, assign=True, name=None) -> List[str]:
+    """skip if none"""
+    if variable is None or (not variable and isinstance(variable, (str))):
+        return []
+    if not name:
+        name = argname("variable")
+    assert isinstance(name, str)
+    if not isinstance(variable, (list, tuple)):
+        variable = [variable]
+    flags = []
+    for v in variable:
+        if v:
+            flag = "--" + (name.replace("_", "-"))
+            if isinstance(v, bool):
+                flags.append(flag)
+            elif assign:
+                flags.append(flag + "=" + str(v))
+            else:
+                flags += [flag, v]
+    return flags
diff --git a/src/xeda/version.py b/src/xeda/version.py
new file mode 100644
index 00000000..5f3c0243
--- /dev/null
+++ b/src/xeda/version.py
@@ -0,0 +1,3 @@
+from importlib.metadata import version
+
+__version__ = version(__package__)
diff --git a/src/xeda/xedaproject.py b/src/xeda/xedaproject.py
new file mode 100644
index 00000000..683affbe
--- /dev/null
+++ b/src/xeda/xedaproject.py
@@ -0,0 +1,59 @@
+from __future__ import annotations
+
+import json
+import os
+from pathlib import Path
+from typing import Dict, List, Optional, Union
+
+import yaml
+
+from .dataclass import XedaBaseModel
+from .design import Design
+from .utils import WorkingDirectory, toml_load
+
+
+class XedaProject(XedaBaseModel):
+    # validate to concrete Designs to verify the whole xedaproject
+    designs: List[Design]
+    # keep raw dict as flows are dynamically discovered
+    flows: Dict[str, dict] = {}
+
+    @classmethod
+    def from_file(cls, file: Union[str, os.PathLike, Path]):
+        """load xedaproject from file"""
+        if not isinstance(file, Path):
+            file = Path(file)
+        ext = file.suffix.lower()
+        if ext == ".toml":
+            data = toml_load(file)
+        else:
+            with open(file) as f:
+                if ext == ".json":
+                    data = json.load(f)
+                elif ext == ".yaml":
+                    data = yaml.safe_load(f)
+                else:
+                    raise ValueError(
+                        f"File {file} has unknown extension {ext}. Supported formats are TOML, JSON, and YAML."
+                    )
+        if not isinstance(data, dict) or not data:
+            raise ValueError("Invalid xedaproject!")
+        designs = data.get("design") or data.get("designs")
+        if not designs:
+            raise ValueError("No designs found in the xedaproject file!")
+
+        flows = data.get("flow") or data.get("flows")
+        with WorkingDirectory(file.parent):
+            return cls(designs=designs, flows=flows)
+
+    @property
+    def design_names(self) -> List[str]:
+        return [d.name for d in self.designs]
+
+    def get_design(self, name: Optional[str] = None) -> Optional[Design]:
+        if name is None:
+            return self.designs[0] if len(self.designs) == 1 else None
+        try:
+            return self.designs[self.design_names.index(name)]
+        except ValueError:
+            return None
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 00000000..25a458ef
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,77 @@
+[tox]
+minversion = 3.20
+envlist = py{38,39,310}, mypy
+requires =
+    tox-gh-actions
+isolated_build = True
+
+[gh-actions]
+python =
+    3.8: py38
+    3.9: py39, mypy
+    3.10: py310
+
+[testenv]
+passenv=HOME
+deps =
+    pytest>7
+    pytest-cov
+    mypy
+    pylint
+    flakeheaven
+    -rexamples/vhdl/sqrt/requirements.txt
+# extras =
+
+commands =
+    pytest
+    flakeheaven lint src/
+
+[testenv:pytest]
+minversion = 7.1
+addopts =
+    --verbose
+    --doctest-modules
+testpaths =
+    tests/
+    # examples/vhdl/sqrt
+    # examples
+asyncio_mode=auto
+deps =
+    pytest>7
+    pytest-cov
+    -rexamples/vhdl/sqrt/requirements.txt
+commands =
+    pytest -s -v
+skip_install = false
+
+
+[testenv:mypy]
+basepython = python3.9
+deps =
+    mypy>=0.940
+    -rtypes-requirements.txt
+commands =
+    mypy src
+skip_install = false
+
+[testenv:pylint]
+deps = pylint
+commands =
+    pylint --rcfile=pyproject.toml src
+
+[flake8]
+exclude = .tox
+
+[pycodestyle]
+max-line-length = 120
+# extend-ignore = D,E203 # D: Docstring errors, E203: see https://github.com/PyCQA/pycodestyle/issues/373 
+ignore =
+    E501
+    line-too-long
+    missing-module-docstring
+    missing-function-docstring
+indent-size = 4
+exclude =
+    .github
+    .eggs
+    .tox
\ No newline at end of file
diff --git a/types-requirements.txt b/types-requirements.txt
new file mode 100644
index 00000000..e7bc323b
--- /dev/null
+++ b/types-requirements.txt
@@ -0,0 +1,7 @@
+types-toml
+types-PyYAML
+types-pkg_resources
+types-setuptools
+types-psutil
+types-mypy-extensions
+types-backports
diff --git a/xeda.png b/xeda.png
new file mode 100644
index 00000000..19a495c9
Binary files /dev/null and b/xeda.png differ
diff --git a/logo.svg b/xeda.svg
similarity index 100%
rename from logo.svg
rename to xeda.svg
diff --git a/xeda/__init__.py b/xeda/__init__.py
deleted file mode 100644
index ef207437..00000000
--- a/xeda/__init__.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from pkg_resources import get_distribution, DistributionNotFound
-
-from . import cli
-
-__project__ = 'xeda'
-__author__ = 'Kamyar Mohajerani'
-__package__ = 'xeda'
-
-try:
-    __version__ = get_distribution(__project__).version
-except DistributionNotFound:
-    __version__ = '(N/A - Local package)'
diff --git a/xeda/__main__.py b/xeda/__main__.py
deleted file mode 100644
index a9693c27..00000000
--- a/xeda/__main__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from . import cli
-
-if __name__ == "__main__":
-    cli.run_xeda()
\ No newline at end of file
diff --git a/xeda/cli.py b/xeda/cli.py
deleted file mode 100644
index 09951356..00000000
--- a/xeda/cli.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# PYTHON_ARGCOMPLETE_OK
-from .xeda_app import XedaApp
-
-def run_xeda(args=None):
-    xeda = XedaApp()
-    xeda.main()
-
diff --git a/xeda/data/boards/ulx3s/board.lpf b/xeda/data/boards/ulx3s/board.lpf
deleted file mode 100644
index b80da181..00000000
--- a/xeda/data/boards/ulx3s/board.lpf
+++ /dev/null
@@ -1,463 +0,0 @@
-## from https://github.com/emard/ulx3s/blob/master/doc/constraints/ulx3s_v20.lpf
-BLOCK RESETPATHS;
-BLOCK ASYNCPATHS;
-## ULX3S v2.x.x and v3.0.x
-
-# The clock "usb" and "gpdi" sheet
-LOCATE COMP "clk_25mhz" SITE "G2";
-IOBUF PORT "clk_25mhz" PULLMODE=NONE IO_TYPE=LVCMOS33;
-FREQUENCY PORT "clk_25mhz" 25 MHZ;
-
-# JTAG and SPI FLASH voltage 3.3V and options to boot from SPI flash
-# write to FLASH possible any time from JTAG:
-#SYSCONFIG CONFIG_IOVOLTAGE=3.3 COMPRESS_CONFIG=ON MCCLK_FREQ=62 MASTER_SPI_PORT=ENABLE SLAVE_SPI_PORT=DISABLE SLAVE_PARALLEL_PORT=DISABLE;
-# write to FLASH possible from user bitstream:
-SYSCONFIG CONFIG_IOVOLTAGE=3.3 COMPRESS_CONFIG=ON MCCLK_FREQ=62 MASTER_SPI_PORT=DISABLE SLAVE_SPI_PORT=DISABLE SLAVE_PARALLEL_PORT=DISABLE;
-
-## USBSERIAL FTDI-FPGA serial port "usb" sheet
-LOCATE COMP "ftdi_rxd" SITE "L4"; # FPGA transmits to ftdi
-LOCATE COMP "ftdi_txd" SITE "M1"; # FPGA receives from ftdi
-LOCATE COMP "ftdi_nrts" SITE "M3"; # FPGA receives
-LOCATE COMP "ftdi_ndtr" SITE "N1"; # FPGA receives
-LOCATE COMP "ftdi_txden" SITE "L3"; # FPGA receives
-IOBUF PORT "ftdi_rxd" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "ftdi_txd" PULLMODE=UP IO_TYPE=LVCMOS33;
-IOBUF PORT "ftdi_nrts" PULLMODE=UP IO_TYPE=LVCMOS33;
-IOBUF PORT "ftdi_ndtr" PULLMODE=UP IO_TYPE=LVCMOS33;
-IOBUF PORT "ftdi_txden" PULLMODE=UP IO_TYPE=LVCMOS33;
-
-## LED indicators "blinkey" and "gpio" sheet
-LOCATE COMP "led[7]" SITE "H3";
-LOCATE COMP "led[6]" SITE "E1";
-LOCATE COMP "led[5]" SITE "E2";
-LOCATE COMP "led[4]" SITE "D1";
-LOCATE COMP "led[3]" SITE "D2";
-LOCATE COMP "led[2]" SITE "C1";
-LOCATE COMP "led[1]" SITE "C2";
-LOCATE COMP "led[0]" SITE "B2";
-IOBUF PORT "led[0]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "led[1]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "led[2]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "led[3]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "led[4]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "led[5]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "led[6]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "led[7]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-
-## Pushbuttons "blinkey", "flash", "power", "gpdi" sheet
-LOCATE COMP "btn[0]" SITE "D6";  # BTN_PWRn (inverted logic)
-LOCATE COMP "btn[1]" SITE "R1";  # FIRE1
-LOCATE COMP "btn[2]" SITE "T1";  # FIRE2
-LOCATE COMP "btn[3]" SITE "R18"; # UP
-LOCATE COMP "btn[4]" SITE "V1";  # DOWN
-LOCATE COMP "btn[5]" SITE "U1";  # LEFT
-LOCATE COMP "btn[6]" SITE "H16"; # RIGHT
-IOBUF PORT "btn[0]" PULLMODE=UP IO_TYPE=LVCMOS33;
-IOBUF PORT "btn[1]" PULLMODE=DOWN IO_TYPE=LVCMOS33;
-IOBUF PORT "btn[2]" PULLMODE=DOWN IO_TYPE=LVCMOS33;
-IOBUF PORT "btn[3]" PULLMODE=DOWN IO_TYPE=LVCMOS33;
-IOBUF PORT "btn[4]" PULLMODE=DOWN IO_TYPE=LVCMOS33;
-IOBUF PORT "btn[5]" PULLMODE=DOWN IO_TYPE=LVCMOS33;
-IOBUF PORT "btn[6]" PULLMODE=DOWN IO_TYPE=LVCMOS33;
-
-## DIP switch "blinkey", "gpio" sheet
-LOCATE COMP "sw[0]" SITE "E8"; # SW1
-LOCATE COMP "sw[1]" SITE "D8"; # SW2
-LOCATE COMP "sw[2]" SITE "D7"; # SW3
-LOCATE COMP "sw[3]" SITE "E7"; # SW4
-IOBUF PORT "sw[0]" PULLMODE=DOWN IO_TYPE=LVCMOS33;
-IOBUF PORT "sw[1]" PULLMODE=DOWN IO_TYPE=LVCMOS33;
-IOBUF PORT "sw[2]" PULLMODE=DOWN IO_TYPE=LVCMOS33;
-IOBUF PORT "sw[3]" PULLMODE=DOWN IO_TYPE=LVCMOS33;
-
-## SPI OLED DISPLAY SSD1331 (Color) or SSD1306 (B/W) "blinkey", "usb" sheet
-LOCATE COMP "oled_clk" SITE "P4";
-LOCATE COMP "oled_mosi" SITE "P3";
-LOCATE COMP "oled_dc" SITE "P1";
-LOCATE COMP "oled_resn" SITE "P2";
-LOCATE COMP "oled_csn" SITE "N2";
-IOBUF PORT "oled_clk" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "oled_mosi" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "oled_dc" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "oled_resn" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "oled_csn" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-
-## SPI Flash chip "flash" sheet
-LOCATE COMP "flash_csn" SITE "R2";
-#LOCATE COMP "flash_clk" SITE "U3";
-LOCATE COMP "flash_mosi" SITE "W2";
-LOCATE COMP "flash_miso" SITE "V2";
-LOCATE COMP "flash_holdn" SITE "W1";
-LOCATE COMP "flash_wpn" SITE "Y2";
-#LOCATE COMP "flash_csspin" SITE "AJ3";
-#LOCATE COMP "flash_initn" SITE "AG4";
-#LOCATE COMP "flash_done" SITE "AJ4";
-#LOCATE COMP "flash_programn" SITE "AH4";
-#LOCATE COMP "flash_cfg_select[0]" SITE "AM4";
-#LOCATE COMP "flash_cfg_select[1]" SITE "AL4";
-#LOCATE COMP "flash_cfg_select[2]" SITE "AK4";
-IOBUF PORT "flash_csn" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-#IOBUF PORT "flash_clk" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "flash_mosi" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "flash_miso" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "flash_holdn" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "flash_wpn" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-#IOBUF PORT "flash_csspin" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-#IOBUF PORT "flash_initn" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-#IOBUF PORT "flash_done" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-#IOBUF PORT "flash_programn" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-#IOBUF PORT "flash_cfg_select[0]" PULLMODE=DOWN IO_TYPE=LVCMOS33 DRIVE=4;
-#IOBUF PORT "flash_cfg_select[1]" PULLMODE=DOWN IO_TYPE=LVCMOS33 DRIVE=4;
-#IOBUF PORT "flash_cfg_select[2]" PULLMODE=DOWN IO_TYPE=LVCMOS33 DRIVE=4;
-
-## SD card "sdcard", "usb" sheet
-LOCATE COMP "sd_clk" SITE "H2"; # sd_clk WiFi_GPIO14
-LOCATE COMP "sd_cmd" SITE "J1"; # sd_cmd_di (MOSI) WiFi GPIO15
-LOCATE COMP "sd_d[0]" SITE "J3"; # sd_dat0_do (MISO) WiFi GPIO2
-LOCATE COMP "sd_d[1]" SITE "H1"; # sd_dat1_irq WiFi GPIO4
-LOCATE COMP "sd_d[2]" SITE "K1"; # sd_dat2 WiFi_GPIO12
-LOCATE COMP "sd_d[3]" SITE "K2"; # sd_dat3_csn WiFi_GPIO13
-LOCATE COMP "sd_wp" SITE "P5"; # not connected
-LOCATE COMP "sd_cdn" SITE "N5"; # not connected
-IOBUF PORT "sd_clk" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sd_cmd" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sd_d[0]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sd_d[1]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sd_d[2]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4; # WiFi GPIO12 pulldown bootstrapping requirement
-IOBUF PORT "sd_d[3]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sd_wp" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sd_cdn" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-
-## ADC SPI (MAX11123) "analog", "ram" sheet
-LOCATE COMP "adc_csn" SITE "R17";
-LOCATE COMP "adc_mosi" SITE "R16";
-LOCATE COMP "adc_miso" SITE "U16";
-LOCATE COMP "adc_sclk" SITE "P17";
-IOBUF PORT "adc_csn" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "adc_mosi" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "adc_miso" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "adc_sclk" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-
-## Audio 4-bit DAC "analog", "gpio" sheet
-# Output impedance 75 ohm.
-# Strong enough to drive 16 ohm earphones.
-LOCATE COMP "audio_l[3]" SITE "B3"; # JACK TIP (left audio)
-LOCATE COMP "audio_l[2]" SITE "C3";
-LOCATE COMP "audio_l[1]" SITE "D3";
-LOCATE COMP "audio_l[0]" SITE "E4";
-LOCATE COMP "audio_r[3]" SITE "C5"; # JACK RING1 (right audio)
-LOCATE COMP "audio_r[2]" SITE "D5";
-LOCATE COMP "audio_r[1]" SITE "B5";
-LOCATE COMP "audio_r[0]" SITE "A3";
-LOCATE COMP "audio_v[3]" SITE "E5"; # JACK RING2 (video or digital audio)
-LOCATE COMP "audio_v[2]" SITE "F5";
-LOCATE COMP "audio_v[1]" SITE "F2";
-LOCATE COMP "audio_v[0]" SITE "H5";
-IOBUF PORT "audio_l[3]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
-IOBUF PORT "audio_l[2]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
-IOBUF PORT "audio_l[1]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
-IOBUF PORT "audio_l[0]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
-IOBUF PORT "audio_r[3]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
-IOBUF PORT "audio_r[2]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
-IOBUF PORT "audio_r[1]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
-IOBUF PORT "audio_r[0]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
-IOBUF PORT "audio_v[3]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
-IOBUF PORT "audio_v[2]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
-IOBUF PORT "audio_v[1]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
-IOBUF PORT "audio_v[0]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
-
-## WiFi ESP-32 "wifi", "usb", "flash" sheet
-# other pins are shared with GP/GN, SD card and JTAG
-LOCATE COMP "wifi_en" SITE "F1"; # enable/reset WiFi
-LOCATE COMP "wifi_rxd" SITE "K3"; # FPGA transmits to WiFi
-LOCATE COMP "wifi_txd" SITE "K4"; # FPGA receives from WiFi
-LOCATE COMP "wifi_gpio0" SITE "L2";
-LOCATE COMP "wifi_gpio5" SITE "N4"; # WIFI LED
-LOCATE COMP "wifi_gpio16" SITE "L1"; # Serial1 RX
-LOCATE COMP "wifi_gpio17" SITE "N3"; # Serial1 TX
-# LOCATE COMP "prog_done" SITE "Y3"; # not GPIO, always active
-IOBUF PORT "wifi_en" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "wifi_rxd" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "wifi_txd" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "wifi_gpio0" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "wifi_gpio5" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "wifi_gpio16" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "wifi_gpio17" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-# IOBUF PORT "prog_done" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-
-## PCB antenna 433 MHz (may be also used for FM) "usb" sheet
-LOCATE COMP "ant_433mhz" SITE "G1";
-IOBUF PORT "ant_433mhz" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-
-## Second USB port "US2" going directly into FPGA "usb", "ram" sheet
-LOCATE COMP "usb_fpga_dp" SITE "E16"; # single ended or differential input only
-LOCATE COMP "usb_fpga_dn" SITE "F16";
-IOBUF PORT "usb_fpga_dp" PULLMODE=NONE IO_TYPE=LVCMOS33D DRIVE=16;
-IOBUF PORT "usb_fpga_dn" PULLMODE=NONE IO_TYPE=LVCMOS33D DRIVE=16;
-LOCATE COMP "usb_fpga_bd_dp" SITE "D15"; # single-ended bidirectional
-LOCATE COMP "usb_fpga_bd_dn" SITE "E15";
-IOBUF PORT "usb_fpga_bd_dp" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "usb_fpga_bd_dn" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-LOCATE COMP "usb_fpga_pu_dp" SITE "B12"; # pull up/down control
-LOCATE COMP "usb_fpga_pu_dn" SITE "C12";
-IOBUF PORT "usb_fpga_pu_dp" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
-IOBUF PORT "usb_fpga_pu_dn" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=16;
-
-## JTAG ESP-32 "usb" sheet
-# connected to FT231X and ESP-32
-# commented out because those are dedicated pins, not directly useable as GPIO
-# but could be used by some vendor-specific JTAG bridging (boundary scan) module
-#LOCATE COMP "jtag_tdi" SITE "R5"; # FTDI_nRI   FPGA receives
-#LOCATE COMP "jtag_tdo" SITE "V4"; # FTDI_nCTS  FPGA transmits
-#LOCATE COMP "jtag_tck" SITE "T5"; # FTDI_nDSR  FPGA receives
-#LOCATE COMP "jtag_tms" SITE "U5"; # FTDI_nDCD  FPGA receives
-#IOBUF PORT "jtag_tdi" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-#IOBUF PORT "jtag_tdo" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-#IOBUF PORT "jtag_tck" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-#IOBUF PORT "jtag_tms" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-
-## SDRAM "ram" sheet
-LOCATE COMP "sdram_clk" SITE "F19";
-LOCATE COMP "sdram_cke" SITE "F20";
-LOCATE COMP "sdram_csn" SITE "P20";
-LOCATE COMP "sdram_wen" SITE "T20";
-LOCATE COMP "sdram_rasn" SITE "R20";
-LOCATE COMP "sdram_casn" SITE "T19";
-LOCATE COMP "sdram_a[0]" SITE "M20";
-LOCATE COMP "sdram_a[1]" SITE "M19";
-LOCATE COMP "sdram_a[2]" SITE "L20";
-LOCATE COMP "sdram_a[3]" SITE "L19";
-LOCATE COMP "sdram_a[4]" SITE "K20";
-LOCATE COMP "sdram_a[5]" SITE "K19";
-LOCATE COMP "sdram_a[6]" SITE "K18";
-LOCATE COMP "sdram_a[7]" SITE "J20";
-LOCATE COMP "sdram_a[8]" SITE "J19";
-LOCATE COMP "sdram_a[9]" SITE "H20";
-LOCATE COMP "sdram_a[10]" SITE "N19";
-LOCATE COMP "sdram_a[11]" SITE "G20";
-LOCATE COMP "sdram_a[12]" SITE "G19";
-LOCATE COMP "sdram_ba[0]" SITE "P19";
-LOCATE COMP "sdram_ba[1]" SITE "N20";
-LOCATE COMP "sdram_dqm[0]" SITE "U19";
-LOCATE COMP "sdram_dqm[1]" SITE "E20";
-LOCATE COMP "sdram_d[0]" SITE "J16";
-LOCATE COMP "sdram_d[1]" SITE "L18";
-LOCATE COMP "sdram_d[2]" SITE "M18";
-LOCATE COMP "sdram_d[3]" SITE "N18";
-LOCATE COMP "sdram_d[4]" SITE "P18";
-LOCATE COMP "sdram_d[5]" SITE "T18";
-LOCATE COMP "sdram_d[6]" SITE "T17";
-LOCATE COMP "sdram_d[7]" SITE "U20";
-LOCATE COMP "sdram_d[8]" SITE "E19";
-LOCATE COMP "sdram_d[9]" SITE "D20";
-LOCATE COMP "sdram_d[10]" SITE "D19";
-LOCATE COMP "sdram_d[11]" SITE "C20";
-LOCATE COMP "sdram_d[12]" SITE "E18";
-LOCATE COMP "sdram_d[13]" SITE "F18";
-LOCATE COMP "sdram_d[14]" SITE "J18";
-LOCATE COMP "sdram_d[15]" SITE "J17";
-IOBUF PORT "sdram_clk" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_cke" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_csn" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_wen" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_rasn" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_casn" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_a[0]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_a[1]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_a[2]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_a[3]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_a[4]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_a[5]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_a[6]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_a[7]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_a[8]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_a[9]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_a[10]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_a[11]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_a[12]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_ba[0]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_ba[1]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_dqm[0]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_dqm[1]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_d[0]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_d[1]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_d[2]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_d[3]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_d[4]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_d[5]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_d[6]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_d[7]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_d[8]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_d[9]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_d[10]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_d[11]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_d[12]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_d[13]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_d[14]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "sdram_d[15]" PULLMODE=NONE IO_TYPE=LVCMOS33 DRIVE=4;
-
-# GPDI differential interface (Video) "gpdi" sheet
-LOCATE COMP "gpdi_dp[0]" SITE "A16"; # Blue +
-LOCATE COMP "gpdi_dn[0]" SITE "B16"; # Blue -
-LOCATE COMP "gpdi_dp[1]" SITE "A14"; # Green +
-LOCATE COMP "gpdi_dn[1]" SITE "C14"; # Green -
-LOCATE COMP "gpdi_dp[2]" SITE "A12"; # Red +
-LOCATE COMP "gpdi_dn[2]" SITE "A13"; # Red -
-LOCATE COMP "gpdi_dp[3]" SITE "A17"; # Clock +
-LOCATE COMP "gpdi_dn[3]" SITE "B18"; # Clock -
-LOCATE COMP "gpdi_ethp" SITE "A19"; # Ethernet +
-LOCATE COMP "gpdi_ethn" SITE "B20"; # Ethernet -
-LOCATE COMP "gpdi_cec" SITE "A18";
-LOCATE COMP "gpdi_sda" SITE "B19"; # I2C shared with RTC
-LOCATE COMP "gpdi_scl" SITE "E12"; # I2C shared with RTC C12->E12
-IOBUF PORT "gpdi_dp[0]" IO_TYPE=LVCMOS33D DRIVE=4;
-IOBUF PORT "gpdi_dn[0]" IO_TYPE=LVCMOS33D DRIVE=4;
-IOBUF PORT "gpdi_dp[1]" IO_TYPE=LVCMOS33D DRIVE=4;
-IOBUF PORT "gpdi_dn[1]" IO_TYPE=LVCMOS33D DRIVE=4;
-IOBUF PORT "gpdi_dp[2]" IO_TYPE=LVCMOS33D DRIVE=4;
-IOBUF PORT "gpdi_dn[2]" IO_TYPE=LVCMOS33D DRIVE=4;
-IOBUF PORT "gpdi_dp[3]" IO_TYPE=LVCMOS33D DRIVE=4;
-IOBUF PORT "gpdi_dn[3]" IO_TYPE=LVCMOS33D DRIVE=4;
-IOBUF PORT "gpdi_ethp" IO_TYPE=LVCMOS33D DRIVE=4;
-IOBUF PORT "gpdi_ethn" IO_TYPE=LVCMOS33D DRIVE=4;
-IOBUF PORT "gpdi_cec" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gpdi_sda" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gpdi_scl" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-
-# GPIO (default single-ended) "gpio", "ram", "gpdi" sheet
-# Physical connector pins:
-# *** when FEMALE ANGLED (90 deg PMOD) soldered ***
-# Jm_n- = Jm_n, Jm_n+ = Jm_n+1
-# example: J1_5- is J1_5 phsyical, J1_5+ is J1_6 physical
-# *** when MALE VERTICAL soldered ***
-# Jm_n+ = Jm_n, Jm_n- = Jm_n+1
-# example: J1_5+ is J1_5 physical, J1_5- is J1_6 physical
-# Pins enumerated gp[0-27], gn[0-27].
-# With differential mode enabled on Lattice,
-# gp[] (+) are used, gn[] (-) are ignored from design
-# as they handle inverted signal by default.
-# To enable differential, rename LVCMOS33->LVCMOS33D
-# To enable clock i/o, add this (example):
-#FREQUENCY PORT "gp[12]" 25.00 MHZ;
-LOCATE COMP "gp[0]" SITE "B11"; # J1_5+  GP0 PCLK
-LOCATE COMP "gn[0]" SITE "C11"; # J1_5-  GN0 PCLK
-LOCATE COMP "gp[1]" SITE "A10"; # J1_7+  GP1 PCLK
-LOCATE COMP "gn[1]" SITE "A11"; # J1_7-  GN1 PCLK
-LOCATE COMP "gp[2]" SITE "A9";  # J1_9+  GP2 GR_PCLK
-LOCATE COMP "gn[2]" SITE "B10"; # J1_9-  GN2 GR_PCLK
-LOCATE COMP "gp[3]" SITE "B9";  # J1_11+ GP3
-LOCATE COMP "gn[3]" SITE "C10"; # J1_11- GN3
-LOCATE COMP "gp[4]" SITE "A7";  # J1_13+ GP4
-LOCATE COMP "gn[4]" SITE "A8";  # J1_13- GN4
-LOCATE COMP "gp[5]" SITE "C8";  # J1_15+ GP5
-LOCATE COMP "gn[5]" SITE "B8";  # J1_15- GN5
-LOCATE COMP "gp[6]" SITE "C6";  # J1_17+ GP6
-LOCATE COMP "gn[6]" SITE "C7";  # J1_17- GN6
-IOBUF PORT "gp[0]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[0]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[1]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[1]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[2]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[2]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[3]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[3]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[4]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[4]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[5]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[5]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[6]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[6]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-LOCATE COMP "gp[7]" SITE "A6";  # J1_23+ GP7
-LOCATE COMP "gn[7]" SITE "B6";  # J1_23- GN7
-LOCATE COMP "gp[8]" SITE "A4";  # J1_25+ GP8
-LOCATE COMP "gn[8]" SITE "A5";  # J1_25- GN8  DIFF
-LOCATE COMP "gp[9]" SITE "A2";  # J1_27+ GP9  DIFF
-LOCATE COMP "gn[9]" SITE "B1";  # J1_27- GN9  DIFF
-LOCATE COMP "gp[10]" SITE "C4";  # J1_29+ GP10 DIFF
-LOCATE COMP "gn[10]" SITE "B4";  # J1_29- GN10 DIFF
-LOCATE COMP "gp[11]" SITE "F4";  # J1_31+ GP11 DIFF WIFI_GPIO26
-LOCATE COMP "gn[11]" SITE "E3";  # J1_31- GN11 DIFF WIFI_GPIO25
-LOCATE COMP "gp[12]" SITE "G3";  # J1_33+ GP12 DIFF WIFI_GPIO33 PCLK
-LOCATE COMP "gn[12]" SITE "F3";  # J1_33- GN12 DIFF WIFI_GPIO32 PCLK
-LOCATE COMP "gp[13]" SITE "H4";  # J1_35+ GP13 DIFF WIFI_GPIO35
-LOCATE COMP "gn[13]" SITE "G5";  # J1_35- GN13 DIFF WIFI_GPIO34
-IOBUF PORT "gp[7]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[7]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[8]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[8]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[9]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[9]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[10]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[10]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[11]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[11]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[12]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[12]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[13]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[13]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-LOCATE COMP "gp[14]" SITE "U18"; # J2_5+  GP14 DIFF ADC1
-LOCATE COMP "gn[14]" SITE "U17"; # J2_5-  GN14 DIFF ADC0
-LOCATE COMP "gp[15]" SITE "N17"; # J2_7+  GP15 DIFF ADC3
-LOCATE COMP "gn[15]" SITE "P16"; # J2_7-  GN15 DIFF ADC2
-LOCATE COMP "gp[16]" SITE "N16"; # J2_9+  GP16 DIFF ADC5
-LOCATE COMP "gn[16]" SITE "M17"; # J2_9-  GN16 DIFF ADC4
-LOCATE COMP "gp[17]" SITE "L16"; # J2_11+ GP17 DIFF ADC7 GR_PCLK
-LOCATE COMP "gn[17]" SITE "L17"; # J2_11- GN17 DIFF ADC6
-LOCATE COMP "gp[18]" SITE "H18"; # J2_13+ GP18 DIFF
-LOCATE COMP "gn[18]" SITE "H17"; # J2_13- GN18 DIFF
-LOCATE COMP "gp[19]" SITE "F17"; # J2_15+ GP19 DIFF
-LOCATE COMP "gn[19]" SITE "G18"; # J2_15- GN19 DIFF
-LOCATE COMP "gp[20]" SITE "D18"; # J2_17+ GP20 DIFF
-LOCATE COMP "gn[20]" SITE "E17"; # J2_17- GN20 DIFF
-IOBUF PORT "gp[14]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[14]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[15]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[15]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[16]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[16]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[17]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[17]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[18]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[18]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[19]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[19]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[20]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[20]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-LOCATE COMP "gp[21]" SITE "C18"; # J2_23+ GP21 DIFF
-LOCATE COMP "gn[21]" SITE "D17"; # J2_23- GN21 DIFF
-LOCATE COMP "gp[22]" SITE "B15"; # J2_25+ GP22
-LOCATE COMP "gn[22]" SITE "C15"; # J2_25- GN22
-LOCATE COMP "gp[23]" SITE "B17"; # J2_27+ GP23
-LOCATE COMP "gn[23]" SITE "C17"; # J2_27- GN23
-LOCATE COMP "gp[24]" SITE "C16"; # J2_29+ GP24
-LOCATE COMP "gn[24]" SITE "D16"; # J2_29- GN24
-LOCATE COMP "gp[25]" SITE "D14"; # J2_31+ GP25
-LOCATE COMP "gn[25]" SITE "E14"; # J2_31- GN25
-LOCATE COMP "gp[26]" SITE "B13"; # J2_33+ GP26
-LOCATE COMP "gn[26]" SITE "C13"; # J2_33- GN26
-LOCATE COMP "gp[27]" SITE "D13"; # J2_35+ GP27
-LOCATE COMP "gn[27]" SITE "E13"; # J2_35- GN27
-IOBUF PORT "gp[21]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[21]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[22]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[22]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[23]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[23]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[24]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[24]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[25]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[25]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[26]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[26]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gp[27]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-IOBUF PORT "gn[27]" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-
-## PROGRAMN (reload bitstream from FLASH, exit from bootloader)
-# PCB v2.0.5 and higher
-LOCATE COMP "user_programn" SITE "M4";
-IOBUF PORT "user_programn" PULLMODE=UP IO_TYPE=LVCMOS33 DRIVE=4;
-
-## SHUTDOWN "power", "ram" sheet (connected from PCB v1.7.5)
-# on PCB v1.7 shutdown is not connected to FPGA
-LOCATE COMP "shutdown" SITE "G16"; # FPGA receives
-IOBUF PORT "shutdown" PULLMODE=DOWN IO_TYPE=LVCMOS33 DRIVE=4;
diff --git a/xeda/data/boards/ulx3s/board.toml b/xeda/data/boards/ulx3s/board.toml
deleted file mode 100644
index 80fd6686..00000000
--- a/xeda/data/boards/ulx3s/board.toml
+++ /dev/null
@@ -1,11 +0,0 @@
-[[board]]
-variant = '85F'
-version = '3.0.8'
-[fpga]
-vendor = 'Lattice'
-part = 'LFE5U-85F-6BG381C'
-
-[clocks.clk_25mhz]
-freq_mhz = 25.0
-
-
diff --git a/xeda/debug.py b/xeda/debug.py
deleted file mode 100644
index 02eafa4f..00000000
--- a/xeda/debug.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from enum import IntEnum
-
-class DebugLevel(IntEnum):
-    NONE = 0
-    LOW = 1
-    MEDIUM = 2
-    HIGH = 3
-    HIGHEST = 4
diff --git a/xeda/defaults.json b/xeda/defaults.json
deleted file mode 100644
index 2447977d..00000000
--- a/xeda/defaults.json
+++ /dev/null
@@ -1,81 +0,0 @@
-{
-    "design": {
-        "language": {
-            "vhdl": {
-                "standard": "08",
-                "synopsys": false
-            }
-        },
-        "rtl": {
-            "top": "LWC",
-            "clock_port": "clk"
-        },
-        "tb":{
-            "top": "LWC_TB",
-            "uut": "uut"
-        }
-    },
-    "flows": {
-        "sim_flow": {
-            "stop_time": null
-        },
-        "diamond_synth": {
-            "fpga_part": "LFE5U-25F-6BG381C",
-            "clock_period": 15.0,
-            "synthesis_engine": "lse",
-            "strategy": "Timing",
-            "timeout": 2600,
-            "fmax_low_freq": 10.0,
-            "fmax_high_freq": 150.0
-        },
-        "vivado_synth": {
-            "clock_period": 5.0,
-            "fpga_part": "xc7a12tcsg325-3",
-            "strategy": "ExtraTiming",
-            "timeout": 3600,
-            "fmax_low_freq": 50.0,
-            "fmax_high_freq": 310.0,
-            "nthreads": 4
-        },
-        "quartus_synth": {
-            "clock_period": 8.0,
-            "fpga_part": "10CL016YU256C6G",
-            "dse": {
-                "num_concurrent": 8,
-                "nproc": null,
-                "num_seeds": 8,
-                "explore": "timing_high_effort",
-                "compile_flow": "fit_sta",
-                "stop_on_success": true,
-                "timeout": "01:00:00"
-            },
-            "project_settings": {
-                "OPTIMIZATION_MODE": "AGGRESSIVE PERFORMANCE",
-                "REMOVE_REDUNDANT_LOGIC_CELLS": "ON",
-                "AUTO_RESOURCE_SHARING": "ON",
-                "ALLOW_REGISTER_RETIMING": "ON",
-                "SYNTH_GATED_CLOCK_CONVERSION": "ON",
-                "FITTER_EFFORT": "STANDARD FIT",
-                "STRATIX_OPTIMIZATION_TECHNIQUE": "SPEED",
-                "CYCLONE_OPTIMIZATION_TECHNIQUE": "SPEED",
-                "PLACEMENT_EFFORT_MULTIPLIER": 3.0,
-                "ROUTER_EFFORT_MULTIPLIER": 3.0,
-                "ROUTER_TIMING_OPTIMIZATION_LEVEL": "MAXIMUM",
-                "FINAL_PLACEMENT_OPTIMIZATION": "ALWAYS",
-                "AUTO_PACKED_REGISTERS_STRATIX": "OFF",
-                "AUTO_PACKED_REGISTERS_CYCLONE": "OFF",
-                "PHYSICAL_SYNTHESIS_COMBO_LOGIC": "ON",
-                "PHYSICAL_SYNTHESIS_REGISTER_DUPLICATION": "ON",
-                "PHYSICAL_SYNTHESIS_REGISTER_RETIMING": "ON",
-                "PHYSICAL_SYNTHESIS_EFFORT": "EXTRA",
-                "AUTO_DSP_RECOGNITION": "OFF",
-                "INNER_NUM": 8
-            }
-        },
-        "dc": {
-            "clock_period": 3.0,
-            "flatten_effort": 1,
-            "alib_dir": "alib_cache"
-        }
-    }
-}
\ No newline at end of file
diff --git a/xeda/flow_runner/__init__.py b/xeda/flow_runner/__init__.py
deleted file mode 100644
index 8f16678d..00000000
--- a/xeda/flow_runner/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .default_runner import *
-from .fmax import FmaxRunner
diff --git a/xeda/flow_runner/default_runner.py b/xeda/flow_runner/default_runner.py
deleted file mode 100644
index 84e3ea9d..00000000
--- a/xeda/flow_runner/default_runner.py
+++ /dev/null
@@ -1,319 +0,0 @@
-import multiprocessing
-import sys
-import re
-import logging
-import pkg_resources
-import json
-
-from ..flows.settings import Settings
-from ..flows.flow import Flow, FlowFatalException, my_print
-from ..utils import camelcase_to_snakecase, load_class, dict_merge, try_convert
-
-logger = logging.getLogger()
-
-def merge_overrides(overrides, settings):
-    if overrides:
-        if isinstance(overrides, str):
-            overrides = [overrides]
-        if len(overrides) == 1:
-            overrides = re.split(r'\s*,\s*', overrides[0])
-        for override in overrides:
-            key, val = override.split('=')
-            hier = key.split('.')
-            patch_dict = dict()
-            for field in hier[:-1]:
-                new_dict = dict()
-                patch_dict[field] = new_dict
-                patch_dict = new_dict
-            patch_dict[hier[-1]] = try_convert(val, convert_lists=True)
-            settings = dict_merge(settings, patch_dict, True)
-    return settings
-
-
-def print_results(results, title, subset):
-    data_width = 32
-    name_width = 80 - data_width
-    hline = "-"*(name_width + data_width)
-
-    my_print("\n" + hline)
-    my_print(f"{title:^{name_width + data_width}s}")
-    my_print(hline)
-    for k, v in results.items():
-        if not k.startswith('_') and (not subset or k in subset):
-            if isinstance(v, float):
-                my_print(f'{k:{name_width}}{v:{data_width}.6f}')
-            elif isinstance(v, bool):
-                my_print(
-                    f'{k:{name_width}}{"True" if v else "False":>{data_width}}')
-            elif isinstance(v, int):
-                my_print(f'{k:{name_width}}{v:>{data_width}}')
-            elif isinstance(v, list):
-                my_print(f'{k:{name_width}}{" ".join(v):<{data_width}}')
-            else:
-                my_print(f'{k:{name_width}}{str(v):>{data_width}s}')
-    my_print(hline + "\n")
-
-
-def run_flow(f: Flow):
-    try:
-        f.run()
-        return f.results
-    except FlowFatalException as e:
-        logger.exception(
-            f'[Run Thread] Fatal exception during flow run in {f.flow_run_dir}: {e}')
-    except KeyboardInterrupt as e:
-        logger.exception(
-            f'[Run Thread] Received KeyboardInterrupt during flow run in {f.flow_run_dir}: {e}')
-    except KeyboardInterrupt as e:
-        logger.exception(
-            f'[Run Thread] Received KeyboardInterrupt during flow run in {f.flow_run_dir}: {e}')
-    return {}
-
-
-class FlowRunner:
-    def __init__(self, args, xeda_project, timestamp) -> None:
-        self.args = args
-        self.timestamp = timestamp
-        self.xeda_project = xeda_project
-
-        if not hasattr(args, 'override_settings'):
-            self.args.override_settings = None
-
-        self.all_settings = self.get_all_settings()
-
-    def get_default_settings(self):
-        defaults_data = pkg_resources.resource_string('xeda', "defaults.json")
-        try:
-            return json.loads(defaults_data)
-        except json.decoder.JSONDecodeError as e:
-            self.fatal(
-                f"Failed to parse defaults settings file (defaults.json): {' '.join(e.args)}", e)
-
-    def fatal(self, msg=None, exception=None):
-        if msg:
-            logger.critical(msg)
-        if exception:
-            raise exception
-        else:
-            raise Exception(msg)
-
-    def validate_settings(self, settings):
-        assert 'design' in settings
-        # design = settings['design']
-        # assert 'sources' in design
-        # assert 'vhdl_std' in design
-        # if design['vhdl_std'] == 8:
-        #     design['vhdl_std'] = "08"
-        # elif design['vhdl_std'] == 2:
-        #     design['vhdl_std'] = "02"
-
-        return settings
-
-    def get_all_settings(self):
-
-        settings = self.get_default_settings()
-
-        def get_design(d):
-            if not isinstance(d, list):
-                return d
-            if len(d) == 1:
-                return d[0]
-            dname = self.args.design
-            if dname:
-                if isinstance(dname, list):
-                    dname = dname[0]  # TODO FIXME match dname !!!!
-                for x in d:
-                    if x['name'] == dname:
-                        return x
-                logger.critical(
-                    f'Design "{dname}" not found in the current project.')
-            else:
-                logger.critical(
-                    f'{len(d)} designs are availables in the current project. Please specify target design using --design')
-            logger.critical(
-                f'Available designs: {", ".join([x["name"] for x in d])}')
-            sys.exit(1)
-
-        design_settings = dict(design=get_design(
-            self.xeda_project['design']), flows=self.xeda_project.get('flows', {}))
-
-        settings = dict_merge(settings, design_settings)
-
-        settings = merge_overrides(self.args.override_settings, settings)
-        flow_settings = settings['flows'].get(self.args.flow, dict())
-        settings['flows'][self.args.flow] = merge_overrides(
-            self.args.override_flow_settings, flow_settings)
-
-        settings['design']['xeda_version'] = self.xeda_project['xeda_version']
-
-        return self.validate_settings(settings)
-
-    # should not override
-    def post_run(self, flow: Flow, print_failed=True):
-        # Run post-run hooks
-        for hook in flow.post_run_hooks:
-            logger.info(
-                f"Running post-run hook from from {hook.__class__.__name__}")
-            hook(flow)
-
-        flow.reports_dir = flow.flow_run_dir / flow.reports_subdir_name
-        if not flow.reports_dir.exists():
-            flow.reports_dir.mkdir(parents=True)
-
-        flow.parse_reports()
-        flow.results['timestamp'] = flow.timestamp
-        flow.results['design.name'] = flow.settings.design['name']
-        flow.results['flow.name'] = flow.name
-        flow.results['flow.run_hash'] = flow.xedahash
-
-        if print_failed or flow.results.get('success'):
-            flow.print_results()
-        flow.dump_results()
-
-        # Run post-results hooks
-        for hook in flow.post_results_hooks:
-            logger.info(
-                f"Running post-results hook from {hook.__class__.__name__}")
-            hook(flow)
-
-    def load_flowclass(self, name: str) -> Flow:
-        splitted = name.split('.')
-        package = ".flows"
-        if len(splitted) > 1:
-            name = splitted[-1]
-            # FIXME TODO merge-in plugin code
-            package = ".plugins." + ".".join(splitted[:-1]) + ".flows"
-        try:
-            return load_class(name, package)
-        except AttributeError as e:
-            self.fatal(
-                f"Could not find Flow class corresponding to {name}. Make sure it's typed correctly.", e)
-
-    def setup_flow(self, flow_settings, design_settings, flow_cls, completed_dependencies=[]) -> Flow:
-
-        if isinstance(flow_cls, str):
-            flow_cls = self.load_flowclass(flow_cls)
-
-        assert issubclass(flow_cls, Flow)
-
-        effective_settings = Settings()
-
-        # override sections
-        effective_settings.design = design_settings
-
-        effective_settings.flow = dict_merge(
-            flow_cls.default_settings, flow_settings)
-
-        # create and initialize the flow object
-        flow: Flow = flow_cls(effective_settings,
-                              self.args, completed_dependencies)
-
-        max_threads = effective_settings.flow.get('nthreads')
-        if not max_threads:
-            max_threads = multiprocessing.cpu_count()
-        flow.nthreads = int(max(1, max_threads))
-
-        flow.prepare()
-
-        return flow
-
-    def get_flow_settings(self, flow_name):
-        return self.all_settings['flows'].get(flow_name, {})
-
-
-class DefaultRunner(FlowRunner):
-    def launch_flow(self, flow_name_or_class, flow_settings, design_settings, force_run):
-        if force_run:
-            logger.info(f"Forced re-run of {flow_name_or_class}")
-
-        flow_class = self.load_flowclass(flow_name_or_class) if isinstance(
-            flow_name_or_class, str) else flow_name_or_class
-
-        completed_dependencies = []
-
-        prerequisite_flows = flow_class.prerequisite_flows(
-            flow_settings, design_settings)
-
-        for prereq, (flow_overrides, design_overrides) in prerequisite_flows.items():
-            prereq_name = prereq if isinstance(
-                prereq, str) else camelcase_to_snakecase(prereq.name)
-
-            parent_overrides = flow_settings.get('dependencies', {}).get(prereq_name, {})
-
-            prereq_flowsettings = dict_merge(self.get_flow_settings(prereq_name), parent_overrides)
-            prereq_flowsettings = dict_merge(prereq_flowsettings, flow_overrides)
-            prereq_design = dict_merge(design_settings, design_overrides)
-
-            logger.info(f"Prerequisite: {prereq.__name__}")
-            # recursive call
-            completed_prereq = self.launch_flow(
-                prereq, prereq_flowsettings, prereq_design, self.args.force_rerun
-            )
-            completed_dependencies.append(completed_prereq)
-
-        flow = self.setup_flow(flow_settings, design_settings, flow_class,
-                               completed_dependencies)
-
-        results_json = flow.flow_run_dir / 'results.json'
-
-        if not force_run:
-            try:
-                with open(results_json) as f:
-                    flow.results = json.load(f)
-            except FileNotFoundError:
-                force_run = True
-                logger.info(
-                    f"Running flow {flow.name} as {results_json} does not exist.")
-            except Exception as e:
-                force_run = True
-                logger.info(f"running flow {flow.name} due to {e}")
-
-            if not force_run and not flow.results.get('success'):
-                force_run = True
-                logger.info(
-                    f"Re-running flow {flow.name} as the previous run was not successful")
-
-            prev_hash = flow.results.get('flow.run_hash')
-            if not force_run and prev_hash != flow.xedahash:
-                force_run = True
-                logger.info(
-                    f"Re-running flow {flow.name} as the previous run hash ({prev_hash}) did not match the current one ({flow.xedahash})")
-
-        if force_run:
-            flow.run_flow()
-            self.post_run(flow)
-            if not flow.results.get('success'):
-                logger.critical(f"{flow.name} failed")
-                exit(1)
-        else:
-            logger.warning(
-                f"Previous results in {results_json} are already up-to-date. Will skip running {flow.name}.")
-            flow.print_results()
-
-        return flow
-
-    def launch(self):
-        flow_name = self.args.flow
-        flow_settings = self.get_flow_settings(flow_name)
-        self.launch_flow(flow_name, flow_settings,
-                         self.all_settings['design'], not self.args.use_stale)
-
-
-def nukemall():
-    def on_terminate(proc):
-        logger.warning(
-            f"Child process {proc.info['name']}[{proc}] terminated with exit code {proc.returncode}")
-
-    try:
-        pass
-        # procs = psutil.Process().children(recursive=True)
-        # print(f"killing {len(procs)} child processes")
-        # for p in procs:
-        #     p.terminate()
-        # gone, alive = psutil.wait_procs(procs, timeout=3, callback=on_terminate)
-        # for p in alive:
-        #     p.kill()
-        # on nix: negative number means the process group with that PGID
-        # os.kill(-os.getpgid(0), signal.SIGINT)
-    except:
-        logger.exception('exception during killing')
diff --git a/xeda/flow_runner/fmax.py b/xeda/flow_runner/fmax.py
deleted file mode 100644
index ddc8d8bb..00000000
--- a/xeda/flow_runner/fmax.py
+++ /dev/null
@@ -1,304 +0,0 @@
-import copy
-import os
-from pebble.common import ProcessExpired
-from pebble.pool.process import ProcessPool
-import random
-import logging
-from concurrent.futures import CancelledError, TimeoutError
-import time
-from pathlib import Path
-import json
-import traceback
-# a heavy overkill, but have plans with in the future
-import numpy
-from math import ceil
-
-from ..utils import unique
-from .default_runner import FlowRunner, print_results
-from ..flows.flow import Flow, FlowFatalException, NonZeroExit
-
-
-logger = logging.getLogger()
-
-
-class Best:
-    def __init__(self, freq, results, settings):
-        self.freq = freq
-        self.results = copy.deepcopy(results)
-        self.settings = copy.deepcopy(settings)
-
-
-def run_flow_fmax(arg):
-    idx: int
-    flow: Flow
-    idx, flow = arg
-    try:
-        flow.run_flow()
-        flow.parse_reports()
-        flow.dump_results()
-        flow.results['timestamp'] = flow.timestamp
-        flow.results['design.name'] = flow.settings.design['name']
-        flow.results['flow.name'] = flow.name
-        flow.results['flow.run_hash'] = flow.xedahash
-        max_luts = flow.settings.flow.get('max_luts')
-        lut = flow.results.get('lut')
-        if max_luts and lut and int(lut) > int(max_luts):
-            flow.results['exceeds_max_luts'] = True
-            idx = None
-        return idx, flow.results, flow.settings, flow.flow_run_dir
-
-    except FlowFatalException as e:
-        logger.warning(
-            f'[Run Thread] Fatal exception during flow run in {flow.flow_run_dir}: {e}')
-        traceback.print_exc()
-        logger.warning(f'[Run Thread] Continuing')
-    except KeyboardInterrupt as e:
-        logger.exception(
-            f'[Run Thread] KeyboardInterrupt received during flow run in {flow.flow_run_dir}')
-        raise e
-    except NonZeroExit as e:
-        logger.warning(f'[Run Thread] {e}')
-    except Exception as e:
-        logger.exception(f"Exception: {e}")
-
-    return None, None, flow.settings, flow.flow_run_dir
-
-
-class FmaxRunner(FlowRunner):
-    def __init__(self, args, xeda_project, timestamp) -> None:
-        super().__init__(args, xeda_project, timestamp)
-        if self.args.force_run_dir:
-            logger.warning("force_run_dir will be disabled in FmaxRunner")
-            self.args.force_run_dir = None
-
-        self.args.xeda_run_dir = os.path.join(self.args.xeda_run_dir, "fmax")
-        logger.warning(f"xeda_run_dir was changed to {self.args.xeda_run_dir}")
-
-    def launch(self):
-        start_time = time.monotonic()
-
-        args = self.args
-        settings = self.all_settings
-
-        flow_name = args.flow
-
-        flow_settings = settings['flows'].get(flow_name)
-        design_settings = settings['design']
-
-        # compatibility with previous key name
-        lo_freq = float(flow_settings.get(
-            'fmax_low', flow_settings.get('fmax_low_freq', 10.0)))
-        hi_freq = float(flow_settings.get(
-            'fmax_high', flow_settings.get('fmax_high_freq', 500.0)))
-
-        flow_settings['fmax_low'] = lo_freq
-        flow_settings['fmax_high'] = hi_freq
-        flow_settings.pop('fmax_low_freq', None)
-        flow_settings.pop('fmax_high_freq', None)
-        
-        assert lo_freq < hi_freq, "fmax_low_freq should be less than fmax_high_freq"
-        resolution = 0.09
-        delta_increment = resolution / 2
-
-        ONE_THOUSAND = 1000.0
-
-        nthreads = int(flow_settings.get('nthreads', 4))
-
-        max_workers = max(2, args.max_cpus // nthreads)
-        logger.info(f'nthreads={nthreads} num_workers={max_workers}')
-        args.quiet = True
-
-        best = None
-        flow_run_dirs = []
-        successful_results = []
-        future = None
-        num_iterations = 0
-        pool = None
-        no_improvements = 0
-
-        previously_tried_frequencies = set()
-        # can be different due to rounding errors, TODO only keep track of periods?
-        previously_tried_periods = set()
-
-        # TODO adaptive tweeking of timeout?
-        proc_timeout_seconds = flow_settings.get('timeout', 3600)
-        logger.info(f'[Fmax] Timeout set to: {proc_timeout_seconds} seconds.')
-
-        error_retries = 0
-
-        def round_freq_to_ps(freq: float) -> float:
-            period = round(ONE_THOUSAND / freq, 3)
-            return ONE_THOUSAND / period
-        try:
-            with ProcessPool(max_workers=max_workers) as pool:
-                while hi_freq - lo_freq >= resolution:
-
-                    finder_retries = 0
-                    while True:
-                        frequencies_to_try, freq_step = numpy.linspace(
-                            lo_freq, hi_freq, num=max_workers, dtype=float, retstep=True)
-
-                        frequencies_to_try = unique([round_freq_to_ps(
-                            f) for f in frequencies_to_try if f not in previously_tried_frequencies])
-
-                        clock_periods_to_try = []
-                        frequencies = []
-                        for freq in frequencies_to_try:
-                            clock_period = round(ONE_THOUSAND / freq, 3)
-                            if clock_period not in previously_tried_periods:
-                                clock_periods_to_try.append(clock_period)
-                                frequencies.append(freq)
-                        frequencies_to_try = frequencies
-
-                        min_required = (
-                            max_workers - max(2, max_workers / 4)) if finder_retries > 20 else max_workers
-
-                        if len(frequencies_to_try) >= max(1, min_required):
-                            break
-                        hi_freq += random.random() * delta_increment
-                        min_lo_freq = best.freq + delta_increment if best else lo_freq
-                        lo_freq = max(min_lo_freq, lo_freq - 0.1 * random.random())
-                        finder_retries += 1
-
-                    logger.info(
-                        f"[Fmax] Trying following frequencies (MHz): {[f'{freq:.2f}' for freq in frequencies_to_try]}")
-
-                    # TODO Just keep clock_periods!
-                    previously_tried_frequencies.update(frequencies_to_try)
-                    previously_tried_periods.update(clock_periods_to_try)
-
-                    flows_to_run = []
-                    for clock_period in clock_periods_to_try:
-                        flow_settings['clock_period'] = clock_period
-                        flow_settings['nthreads'] = nthreads
-                        flow = self.setup_flow(
-                            flow_settings, design_settings, flow_name)
-
-                        flow.no_console = True
-                        flows_to_run.append(flow)
-
-                    future = pool.map(run_flow_fmax, enumerate(
-                        flows_to_run), timeout=proc_timeout_seconds)
-                    num_iterations += 1
-
-                    improved_idx = None
-
-                    try:
-                        iterator = future.result()
-                        if not iterator:
-                            logger.error("iterator is None! Retrying")
-                            if error_retries < 4:
-                                error_retries += 1
-                                continue  # retry
-                            else:
-                                logger.error("error_retries > MAX, exiting")
-                                break
-                        while True:
-                            try:
-                                idx, results, fs, flow_run_dir = next(iterator)
-                                flow_run_dirs.append(flow_run_dir)
-                                if idx and results:
-                                    freq = frequencies_to_try[idx]
-                                    if results['success']:
-                                        error_retries = 0
-                                        r = {k: results.get(k) for k in (
-                                            'clock_period', 'clock_frequency', 'wns', 'lut', 'ff', 'slice')}
-                                        r['flow_run_dir'] = flow_run_dir
-                                        successful_results.append(r)
-                                        if not best or freq > best.freq:
-                                            best = Best(freq, results, fs)
-                                            improved_idx = idx
-                            except StopIteration:
-                                break # inner while True
-                            except TimeoutError as e:
-                                logger.critical(
-                                    f"Flow run took longer than {e.args[1]} seconds. Cancelling remaining tasks.")
-                                future.cancel()
-                            except ProcessExpired as e:
-                                logger.critical(
-                                    f"{e}. Exit code: {e.exitcode}")
-                    except CancelledError:
-                        logger.warning("[Fmax] CancelledError")
-                    except KeyboardInterrupt:
-                        pool.stop()
-                        pool.join()
-                        raise
-
-                    if freq_step < resolution * 0.5:
-                        logger.info(
-                            f"Stopping: freq_step={freq_step} is below the limit")
-                        break
-
-                    if not best or improved_idx is None:
-                        no_improvements += 1
-                        if no_improvements > 4:
-                            logger.info(
-                                f"Stopping no viable frequencies found after {no_improvements} tries.")
-                            break
-                        logger.info(f"No improvements during this iteration.")
-                        if not best:
-                            hi_freq = lo_freq + resolution
-                            shrink_factor = 0.7 + no_improvements
-                            lo_freq /= shrink_factor
-                        else:
-                            if no_improvements > 2 and freq_step < no_improvements * resolution:
-                                logger.info(
-                                    f"Stopping as there were no improvements in {no_improvements} consecutive iterations.")
-                                break
-                            hi_freq = best.freq + 1.5 * max(freq_step, 1 + max_workers * resolution)
-                            lo_freq = (lo_freq + best.freq) / 2
-                    else:
-                        lo_freq = best.freq + delta_increment + delta_increment * random.random()
-                        no_improvements = 0
-                        # last or one before last
-                        if improved_idx >= (len(frequencies_to_try) // 2) or frequencies_to_try[-1] - best.freq <= freq_step:
-                            min_plausible_period = (
-                                ONE_THOUSAND / best.freq) - best.results['wns'] - 0.001
-                            lo_point_choice = frequencies_to_try[1] if len(
-                                frequencies_to_try) > 4 else frequencies_to_try[0]
-                            hi_freq = max(best.freq + min(max_workers * 1.0, best.freq -
-                                                          lo_point_choice),  ceil(ONE_THOUSAND / min_plausible_period))
-                        else:
-                            hi_freq = (hi_freq + best.freq + freq_step) / 2
-
-                        hi_freq += 1
-
-                    hi_freq = ceil(hi_freq)
-
-                    logger.info(f'[Fmax] End of iteration #{num_iterations}')
-                    logger.info(
-                        f'[Fmax] Execution Time so far: {int(time.monotonic() - start_time) // 60} minute(s)')
-                    if best and best.results:
-                        print_results(best.results, title='Best so far', subset=[
-                                      'clock_period', 'clock_frequency', 'wns', 'lut', 'ff', 'slice'])
-                    logger.info(f"Next iteration: [{lo_freq} ... {hi_freq}] resolution={resolution}")
-
-        except KeyboardInterrupt:
-            logger.exception('Received Keyboard Interrupt')
-        except Exception as e:
-            logger.exception(f'Received exception: {e}')
-            traceback.print_exc()
-        finally:
-            if future and not future.cancelled():
-                future.cancel()
-            if pool:
-                pool.close()
-                pool.join()
-            runtime_minutes = int(time.monotonic() - start_time) // 60
-            if best:
-                best.iterations = num_iterations
-                best.runtime_minutes = runtime_minutes
-                print_results(best.results, title='Best Results', subset=[
-                    'clock_period', 'clock_frequency', 'lut', 'ff', 'slice'])
-                best_json_path = Path(args.xeda_run_dir) / \
-                    f'fmax_{settings["design"]["name"]}_{flow_name}_{self.timestamp}.json'
-                logger.info(f"Writing best result to {best_json_path}")
-
-                with open(best_json_path, 'w') as f:
-                    json.dump(dict(best=best, successful_results=successful_results, flow_run_dirs=flow_run_dirs), f, default=lambda x: x.__dict__ if hasattr(
-                        x, '__dict__') else str(x), indent=4)
-            else:
-                logger.warning("No successful results.")
-            logger.info(
-                f'[Fmax] Total Execution Time: {runtime_minutes} minute(s)')
-            logger.info(f'[Fmax] Total Iterations: {num_iterations}')
diff --git a/xeda/flows/__init__.py b/xeda/flows/__init__.py
deleted file mode 100644
index 8c8def9c..00000000
--- a/xeda/flows/__init__.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# © 2020 [Kamyar Mohajerani](mailto:kamyar@ieee.org)
-
-# all Flow classes imported here can be used from FlowRunners and will be reported on the command-line help
-from .vivado import *
-from .quartus import QuartusSynth
-from .diamond import DiamondSynth
-from .ghdl import GhdlSim
-from .modelsim import Modelsim
-from .dc import Dc
-from .yosys import Yosys, NextPnr, OpenFpgaLoader
-
diff --git a/xeda/flows/dc/__init__.py b/xeda/flows/dc/__init__.py
deleted file mode 100644
index 1f9e8cdc..00000000
--- a/xeda/flows/dc/__init__.py
+++ /dev/null
@@ -1,171 +0,0 @@
-# © 2020 [Kamyar Mohajerani](mailto:kamyar@ieee.org)
-
-import logging
-from pathlib import Path
-from typing import Mapping
-from types import SimpleNamespace
-from ..flow import SynthFlow
-import re
-import sys, os
-import toml
-from glob import glob
-from ...utils import dict_merge
-
-logger = logging.getLogger()
-
-
-def get_hier(dct, dotted_path, default=None):
-    splitted = dotted_path.split('.')
-    merged_leaves = {}
-    for i,key in enumerate(splitted):
-        try:
-            for k,v in dct.items():
-                if not isinstance(v, Mapping):
-                    merged_leaves[k] = v
-            dct = dct[key]
-        except KeyError:
-            print(f'Key {key} not found in {".".join(splitted[:i])}!')
-            return default
-            
-    for k,v in dct.items():
-        # if not isinstance(v, Mapping):
-        merged_leaves[k] = v
-    return SimpleNamespace(**merged_leaves)
-
-
-class Dc(SynthFlow):
-
-    def run(self):
-        self.nthreads = min(self.nthreads, 16)
-        adk_id = self.settings.flow.get('adk')
-        if not adk_id:
-            self.fatal(f'Flow setting: dc.adk was not specified!')
-        logger.info(f'adk_id={adk_id}')
-        adk_root = Path.home() / "adk"
-        adk_config = {}
-        for toml_file in adk_root.glob('*.toml'):
-            with open(toml_file) as f:
-                adk_config = dict_merge(adk_config, toml.loads(f.read()), add_keys=True)
-
-        adk = get_hier(adk_config, adk_id)
-
-        if not os.path.isabs(adk.path):
-            adk.path = os.path.join(str(adk_root), adk.path)
-
-        print(adk)
-
-        script_path = self.copy_from_template(f'run.tcl',
-                                              synth_output_dir='output',
-                                              adk=adk,
-                                              )
-
-        self.run_process('dc_shell-xg-t', ['-64bit', '-topographical_mode', '-f', script_path],
-                         stdout_logfile='dc_stdout.log',
-                         check=True
-                         )
-
-    def parse_reports(self):
-        reports_dir = self.reports_dir
-        top_name = self.settings.design['rtl'].get('top', "TOP")
-
-        failed = False
-
-        self.parse_report_regex(reports_dir / f'{top_name}.mapped.area.rpt',
-                          r'Number of ports:\s*(?P<num_ports>\d+)',
-                          r'Number of nets:\s*(?P<num_nets>\d+)',
-                          r'Number of cells:\s*(?P<num_cells>\d+)',
-                          r'Number of combinational cells:\s*(?P<num_cells_combinational>\d+)',
-                          r'Number of sequential cells:\s*(?P<num_cells_sequentual>\d+)',
-                          r'Number of macros/black boxes:\s*(?P<num_macro_bbox>\d+)',
-                          r'Number of buf/inv:\s*(?P<num_buf_inv>\d+)',
-                          r'Number of references:\s*(?P<num_refs>\d+)',
-                          r'Combinational area:\s*(?P<area_combinational>\d+(?:\.\d+)?)',
-                          r'Buf/Inv area:\s*(?P<area_buf_inv>\d+(?:\.\d+)?)',
-                          r'Noncombinational area:\s*(?P<area_noncombinational>\d+(?:\.\d+)?)',
-                          r'Macro/Black Box area:\s*(?P<area_macro_bbox>\d+(?:\.\d+)?)',
-                          r'Net Interconnect area:\s*(?P<area_interconnect>\S+.*$)',
-                          r'Total cell area:\s*(?P<area_cell_total>\d+(?:\.\d+)?)',
-                          r'Total area:\s*(?P<area_macro_bbox>\w+)',
-                          r'Core Area:\s*(?P<area_core>\d+(?:\.\d+)?)',
-                          r'Aspect Ratio:\s*(?P<aspect_ratio>\d+(?:\.\d+)?)',
-                          r'Utilization Ratio:\s*(?P<utilization_ratio>\d+(?:\.\d+)?)',
-                          dotall=False
-                          )
-
-        reportfile_path = reports_dir / f'{top_name}.mapped.qor.rpt'
-
-
-        def try_convert(s):
-            s = s.strip()
-            try:
-                return int(s)
-            except:
-                try:
-                    return float(s)
-                except:
-                    return s
-
-
-        def parse_kvs(kvs):
-            kvs = re.split(r'\s*\n\s*', kvs)
-            kvs = [re.split(r'\s*:\s*', s.strip()) for s in kvs if s.strip()]
-            return {s[0].strip(): try_convert(s[1]) for s in kvs}
-
-        path_group_re = re.compile(
-            r"^\s*Timing Path Group\s+'(?P<path_group_name>\w+)'\n\s*\-+\s*\n(?P<kv>(?:^.*\n)+)", re.MULTILINE)
-
-
-        area_re = re.compile(r"^\s*Area\s*\n\s*\-+\s*\n(?P<kv1>(?:^.*\n)+)\s*\-+\s*\n(?P<kv2>(?:^.*\n)+)", re.MULTILINE)
-        drc_re = re.compile(r"^\s*Design Rules\s*\n\s*\-+\s*\n(?P<kv>(?:^.*\n)+)", re.MULTILINE)
-        wns_re = re.compile(
-            r"^\s*Design\s+WNS:\s+(?P<wns>\d+\.\d+)\s+TNS:\s+(?P<tns>\d+\.\d+)\s+Number of Violating Paths:\s*(?P<nvp>\d+)")
-        hold_wns_re = re.compile(
-            r"^\s*Design\s+\(Hold\)\s+WNS:\s+(?P<wns>\d+\.\d+)\s+TNS:\s+(?P<tns>\d+\.\d+)\s+Number of Violating Paths:\s*(?P<nvp>\d+)")
-
-        # placeholder for ordering
-        self.results['path_groups'] = None
-
-        with open(reportfile_path) as rpt_file:
-            content = rpt_file.read()
-            sections = re.split(r'\n\s*\n', content)
-
-            path_groups = dict()
-            for sec in sections:
-                match = path_group_re.match(sec)
-                if match:
-                    group_name = match.group('path_group_name')
-                    path_groups[group_name] = parse_kvs(match.group('kv'))
-
-                else:
-                    match = area_re.match(sec)
-                    if match:
-                        kv1 = parse_kvs(match.group('kv1'))
-                        kv2 = parse_kvs(match.group('kv2'))
-                        self.results['area'] = {**kv1, **kv2}
-                    else:
-                        match = drc_re.match(sec)
-                        if match:
-                            drc = parse_kvs(match.group('kv'))
-                            self.results['drc'] = drc
-                            if drc['Nets With Violations'] != 0:
-                                print(f"Nets With DRC Violations: {drc['Nets With Violations']}")
-                        else:
-                            match = wns_re.match(sec)
-                            if match:
-                                self.results['wns'] = float(match.group('wns'))
-                                self.results['tns'] = float(match.group('tns'))
-                                self.results['num_violating_paths'] = int(match.group('nvp'))
-                                if self.results['wns'] > 0 or self.results['tns'] > 0 or self.results['num_violating_paths'] != 0:
-                                    failed = True
-                            else:
-                                match = hold_wns_re.match(sec)
-                                if match:
-                                    self.results['hold_wns'] = float(match.group('wns'))
-                                    self.results['hold_tns'] = float(match.group('tns'))
-                                    self.results['hold_num_violating_paths'] = int(match.group('nvp'))
-                                    if self.results['hold_wns'] > 0 or self.results['hold_tns'] > 0 or self.results['hold_num_violating_paths'] != 0:
-                                        failed = True
-            self.results['path_groups'] = path_groups
-
-        self.results['success'] = not failed
-
diff --git a/xeda/flows/diamond/__init__.py b/xeda/flows/diamond/__init__.py
deleted file mode 100644
index 705db141..00000000
--- a/xeda/flows/diamond/__init__.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# © 2020 [Kamyar Mohajerani](mailto:kamyar@ieee.org)
-
-from ..flow import Flow, SynthFlow
-
-
-class Diamond(Flow):
-    reports_subdir_name = 'diamond_impl'
-
-
-class DiamondSynth(Diamond, SynthFlow):
-    default_settings = {**SynthFlow.default_settings,
-                        'impl_folder': 'diamond_impl',
-                        'impl_name': 'Implementation0',
-                        'nthreads': 2,
-                        'syn_cmdline_args': None}
-
-    def run(self):
-        constraint_exts = ['ldc'] if self.settings.flow["synthesis_engine"] == "lse" else ['sdc', 'fdc']
-        constraints = [f'constraints.{ext}' for ext in constraint_exts]
-        for constraint in constraints:
-            self.copy_from_template(constraint)
-        script_path = self.copy_from_template(f'synth.tcl')
-        self.run_process('diamondc', [str(script_path)])
-
-    def parse_reports(self):
-        self.results = dict()
-        reports_dir = self.flow_run_dir / 'diamond_impl'
-        design_name = self.settings.design['name']
-        impl_name = self.settings.flow['impl_name']
-
-        period_pat = r'''^\s*Preference:\s+PERIOD\s+PORT\s+\"(?P<clock_port>\w+)\"\s+(?P<clock_period>\d+\.\d+)\s+ns.*HIGH\s+\d+\.\d+\s+ns\s*;\s*
-\s*\d+\s+items\s+\S+\s+(?P<_timing_errors>\d+)\s+timing\s+errors?'''
-        freq_pat = r'^\s*Preference:\s+FREQUENCY\s+PORT\s+\"(?P<clock_port>\w+)\"\s+(?P<clock_frequency>\d+\.\d+)\s+MHz\s*;\s*\n\s*\d+\s+items\s+\S+\s+(?P<_timing_errors>\d+)\s+timing\s+errors?'
-        self.parse_report_regex(reports_dir / f'{design_name}_{impl_name}.twr', [period_pat, freq_pat])
-
-        if 'clock_frequency' in self.results:
-            frequency = self.results['clock_frequency']
-            period = 1000.0/frequency
-            self.results['clock_period'] = period
-
-        else:
-            period = self.results['clock_period']
-            frequency = 1000.0/period
-            self.results['clock_frequency'] = frequency
-
-        slice_pat = r'^Device\s+utilization\s+summary:\s*.*^\s+SLICE\s+(?P<slice>\d+)\/(?P<_slice_avail>\d+).*^Number\s+of\s+Signals'
-        time_pat = r'''Level\s*/\s+Number\s+Worst\s+Timing\s+Worst\s+Timing\s+Run\s+NCD\s*
-\s*Cost\s+\[ncd\]\s+Unrouted\s+Slack\s+Score\s+Slack\(hold\)\s+Score\(hold\)\s+Time\s+Status\s*
-(\s*\-+){8}\s*
-\s*(?P<_lvl_cost>\S+)\s+(?P<_ncd>\S+)\s+(?P<_num_unrouted>\d+)\s+(?P<wns>\-?\d+\.\d+)\s+(?P<_setup_score>\d+)\s+(?P<whs>\-?\d+\.\d+)\s+(?P<_hold_score>\d+)\s+(?P<_runtime>\d+(?:\:\d+)*)\s+(?P<_status>\w+)\s*$'''
-        self.parse_report_regex(reports_dir / f'{design_name}_{impl_name}.par', slice_pat, time_pat)
-
-        # NOTE there can be "page breaks" anywhere in the mrp file (others? TODO)
-        # NOTE therefore only match lines
-        #   1. Total number of LUT4s = (Number of logic LUT4s) + 2*(Number of distributed RAMs) + 2*(Number of ripple logic)
-        #   2. Number of logic LUT4s does not include count of distributed RAM and ripple logic.
-        slice_pattern = r'''\s*Number\s+of\s+registers:\s*(?P<ff>\d+)\s+out\s+of\s*(?P<_ff_avail>\d+).*
-\s*Number\s+of\s+SLICEs:\s*(?P<_slice_map>\d+)\s*out\s+of\s*(?P<_slice_avail>\d+).*
-\s+SLICEs\s+as\s+RAM:\s*(?P<_slice_ram>\d+)\s*out\s+of\s*(?P<_slice_ram_avail>\d+).*
-\s+SLICEs\s+as\s+Carry:\s*(?P<_slice_carry>\d+)\s+out\s+of\s+(?P<_slice_carry_avail>\d+).*
-\s*Number\s+of\s+LUT4s:\s*(?P<lut>\d+)\s+out of\s+(?P<_lut_avail>\d+).*
-\s+Number\s+used\s+as\s+logic\s+LUTs:\s*(?P<_lut_logic>\d+)\s*
-\s+Number\s+used\s+as\s+distributed\s+RAM:\s*(?P<_lut_dram>\d+)\s*
-\s+Number\s+used\s+as\s+ripple\s+logic:\s*(?P<_lut_ripple>\d+)\s*
-\s+Number\s+used\s+as\s+shift\s+registers:\s*(?P<_lut_shift>\d+)\s*.*
-\s*Number\s+of\s+block\s+RAMs:\s*(?P<bram>\d+)\s+out\s+of\s+(?P<_bram_avail>\d+).*'''
-
-        dsp_pattern = r'''\s+MULT18X18D\s+(?P<_dsp_MULT18X18D>\d+)\s*.*
-\s+MULT9X9D\s+(?P<_dsp_MULT9X9D>\d+)\s*.*'''
-
-        self.parse_report_regex(reports_dir / f'{design_name}_{impl_name}.mrp', slice_pattern, dsp_pattern)
-
-        # FIXME add other types of available ALUs and DSPs
-        self.results['dsp'] = self.results['_dsp_MULT18X18D'] + self.results['_dsp_MULT9X9D']
-
-        failed = False
-
-        # TODO FIXME move to LwcSynth
-        forbidden_resources = ['dsp', 'bram']
-        for res in forbidden_resources:
-            if (res in self.results and self.results[res] != 0):
-                self.logger.critical(f'Map report shows {self.results[res]} use(s) of forbidden resource {res}.')
-                failed = True
-
-        failed = failed or (self.results['wns'] < 0) or (self.results['whs'] < 0) or (
-            self.results['_num_unrouted'] != 0) or (self.results['_status'].lower() != 'completed') or (
-                self.results['_timing_errors'] != 0)
-
-        self.results['success'] = not failed
diff --git a/xeda/flows/flow.py b/xeda/flows/flow.py
deleted file mode 100644
index f5b3ef2a..00000000
--- a/xeda/flows/flow.py
+++ /dev/null
@@ -1,583 +0,0 @@
-# © 2020 [Kamyar Mohajerani](mailto:kamyar@ieee.org)
-
-import copy
-from datetime import datetime
-import json
-import os
-import re
-from pathlib import Path
-import subprocess
-# from contextlib import contextmanager
-import time
-from types import SimpleNamespace
-from jinja2 import Environment, PackageLoader, StrictUndefined
-import logging
-from jinja2.loaders import ChoiceLoader
-from progress import SHOW_CURSOR
-from progress.spinner import Spinner as Spinner
-import colored
-import hashlib
-from typing import Mapping, Union, Dict, List
-
-from .settings import Settings
-from ..utils import camelcase_to_snakecase, try_convert
-from ..debug import DebugLevel
-
-logger = logging.getLogger()
-
-JsonType = Union[str, int, float, bool, List['JsonType'], 'JsonTree']
-JsonTree = Dict[str, JsonType]
-StrTreeType = Union[str, List['StrTreeType'], 'StrTree']
-StrTree = Dict[str, StrTreeType]
-
-
-class FlowFatalException(Exception):
-    """Fatal error"""
-    pass
-
-
-class NonZeroExit(Exception):
-    """Process exited with non-zero return"""
-    pass
-
-
-def final_kill(proc):
-    try:
-        proc.terminate()
-        proc.wait()
-        proc.kill()
-        proc.wait()
-    except:
-        pass
-
-# @contextmanager
-# def process(*args, **kwargs):
-#     proc = subprocess.Popen(*args, **kwargs)
-#     try:
-#         yield proc
-#     finally:
-#         final_kill(proc)
-
-
-def my_print(*args, **kwargs):
-    print(*args, **kwargs)
-
-
-# similar to str.removesuffix in Python 3.9+
-def removesuffix(s: str, suffix: str) -> str:
-    return s[:-len(suffix)] if suffix and s.endswith(suffix) else s
-
-def removeprefix(s: str, suffix: str) -> str:
-    return s[len(suffix):] if suffix and s.startswith(suffix) else s
-
-
-class Flow():
-    """ A flow may run one or more tools and is associated with a single set of settings and a single design. """
-
-    required_settings = {}
-    default_settings = {}
-    reports_subdir_name = 'reports'
-    timeout = 3600 * 2  # in seconds
-    name = None
-
-    @classmethod
-    def prerequisite_flows(cls, flow_settings, design_settings):
-        return {}
-
-    def __init_subclass__(cls) -> None:
-        cls_name = camelcase_to_snakecase(cls.__name__)
-        mod_name = cls.__module__
-        if mod_name and not mod_name.startswith('xeda.flows.'):
-            mod_name = removeprefix(mod_name, "xeda.plugins.")
-            m = mod_name.split('.')  # FIXME FIXME FIXME!!!
-            cls_name =  m[0] + "." + cls_name
-        cls.name = cls_name
-
-    def __init__(self, settings: Settings, args: SimpleNamespace, completed_dependencies: List['Flow']):
-
-        self.args = args
-        self.xedahash = None
-        self.run_path = None
-        self.xeda_run_dir = Path(args.xeda_run_dir)
-        self.results_dir = self.xeda_run_dir / 'Results' / self.name
-        self.results_dir.mkdir(exist_ok=True, parents=True)
-        self.flow_run_dir = None
-        self.reports_dir = None
-        self.init_time = 0
-
-        self.settings = settings
-        self.nthreads = int(settings.flow.get('nthreads', 1))
-
-        self.results = dict()
-        self.results['success'] = False
-
-        self.jinja_env = Environment(
-            loader=ChoiceLoader(
-                [
-                    PackageLoader(self.__module__, 'templates'),
-                    PackageLoader(self.__class__.__module__, 'templates'),
-                ] +
-                [
-                    PackageLoader(clz.__module__, 'templates') for clz in self.__class__.__bases__
-                ]
-            ),
-            autoescape=False,
-            undefined=StrictUndefined
-        )
-
-        self.no_console = args.debug >= DebugLevel.LOW
-        self.post_run_hooks = []
-        self.post_results_hooks = []
-
-        self.completed_dependencies = completed_dependencies
-
-    def run_flow(self):
-        self.prepare()
-
-        if not self.flow_run_dir.exists():
-            self.flow_run_dir.mkdir(parents=True)
-        else:
-            logger.warning(
-                f'Using existing run directory: {self.flow_run_dir}')
-
-        assert self.flow_run_dir.is_dir()
-
-        self.check_settings()
-        self.dump_settings()
-
-        self.timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S")
-        self.init_time = time.monotonic()
-        self.run()
-
-    def gen_xeda_hash(self):
-        def semantic_hash(data: JsonTree, hasher=hashlib.sha1) -> str:
-            def get_digest(b: bytes):
-                return hasher(b).hexdigest()[:32]
-
-            # data: JsonType, not adding type as Pylance does not seem to like recursive types :/
-            def sorted_dict_str(data) -> StrTreeType:
-                if isinstance(data, Mapping):
-                    return {k: sorted_dict_str(data[k]) for k in sorted(data.keys())}
-                elif isinstance(data, list):
-                    return [sorted_dict_str(val) for val in data]
-                elif hasattr(data, '__dict__'):
-                    return sorted_dict_str(data.__dict__)
-                else:
-                    return str(data)
-
-            return get_digest(bytes(repr(sorted_dict_str(data)), 'UTF-8'))
-
-        try:
-            return semantic_hash(self.settings)
-        except FileNotFoundError as e:
-            self.fatal(f"Semantic hash failed: {e} ")
-
-    def check_settings(self):
-        for req_key, req_type in self.required_settings.items():
-            if req_key not in self.settings.flow:
-                self.fatal(f'{req_key} is required to be set for {self.name}')
-            # else:
-                # val = self.settings.flow[req_key]
-                # if (typing.get_origin(req_type) is Union and not isinstance(val, typing.get_args(req_type))) or not isinstance(val,  req_type):
-                #     self.fatal(f'{req_key} should have type `{req_type.__name__}` for {self.name}')
-
-    def prepare(self):
-        design_settings = self.settings.design
-
-        for section in ['rtl', 'tb']:
-            section_settings = design_settings.get(section)
-            if section_settings:
-                section_settings['sources'] = [
-                    DesignSource(src) if isinstance(src, str) else src for src in section_settings.get('sources', [])
-                ]
-
-                generics = section_settings.get("generics", {})
-                for gen_key, gen_val in generics.items():
-                    if isinstance(gen_val, dict) and 'file' in gen_val:
-                        path = gen_val['file']
-                        logger.debug(
-                            f'Generic `{gen_key}` marked with `file` attribute is treated as FileResource({path})')
-                        generics[gen_key] = FileResource(path)
-
-        # all design flow-critical settings should be fixed from this point onwards
-
-        self.xedahash = self.gen_xeda_hash()
-
-        self.run_path = Path(self.args.force_run_dir) if self.args.force_run_dir else (
-            self.xeda_run_dir / '.xeda_run' / self.xedahash)
-
-        self.run_path = self.run_path.resolve()
-
-        self.flow_run_dir = self.run_path / self.name
-        self.reports_dir = self.flow_run_dir / self.reports_subdir_name
-
-    def run(self):
-        # Must be implemented
-        # extraneous `if` needed for Pylace
-        if self:
-            raise NotImplementedError
-
-    def parse_reports(self):
-        # Do nothing if not implemented
-        pass
-
-    def dump_settings(self):
-        effective_settings_json = self.flow_run_dir / f'settings.json'
-        logger.info(f'dumping effective settings to {effective_settings_json}')
-        self.dump_json(self.settings, effective_settings_json)
-
-    def copy_from_template(self, resource_name, **kwargs):
-        template = self.jinja_env.get_template(resource_name)
-        script_path = self.flow_run_dir / resource_name
-        logger.debug(f'generating {script_path.resolve()} from template.')
-        rendered_content = template.render(flow=self.settings.flow,
-                                           design=self.settings.design,
-                                           project=self.settings.project,
-                                           nthreads=self.nthreads,
-                                           debug=self.args.debug,
-                                           reports_dir=self.reports_subdir_name,
-                                           **kwargs)
-        with open(script_path, 'w') as f:
-            f.write(rendered_content)
-        return resource_name
-
-    def conv_to_relative_path(self, src):
-        path = Path(src).resolve(strict=True)
-        return os.path.relpath(path, self.flow_run_dir)
-
-    def fatal(self, msg):
-        logger.critical(msg)
-        raise FlowFatalException(msg)
-
-    def run_process(self, prog, prog_args, check=True, stdout_logfile=None, initial_step=None, force_echo=False, nolog=False):
-        prog_args = [str(a) for a in prog_args]
-        if nolog:
-            subprocess.check_call([prog] + prog_args, cwd=self.flow_run_dir)
-            return
-        if not stdout_logfile:
-            stdout_logfile = f'{prog}_stdout.log'
-        proc = None
-        spinner = None
-        unicode = True
-        verbose = not self.args.quiet and (self.args.verbose or force_echo)
-        echo_instructed = False
-        stdout_logfile = self.flow_run_dir / stdout_logfile
-        start_step_re = re.compile(
-            r'^={12}=*\(\s*(?P<step>[^\)]+)\s*\)={12}=*')
-        enable_echo_re = re.compile(r'^={12}=*\( \*ENABLE ECHO\* \)={12}=*')
-        disable_echo_re = re.compile(r'^={12}=*\( \*DISABLE ECHO\* \)={12}=*')
-        error_msg_re = re.compile(r'^\s*error:?\s+', re.IGNORECASE)
-        warn_msg_re = re.compile(r'^\s*warning:?\s+', re.IGNORECASE)
-        critwarn_msg_re = re.compile(
-            r'^\s*critical\s+warning:?\s+', re.IGNORECASE)
-
-        def make_spinner(step):
-            if self.no_console:
-                return None
-            return Spinner('⏳' + step + ' ' if unicode else step + ' ')
-
-        redirect_std = self.args.debug < DebugLevel.HIGH
-        with open(stdout_logfile, 'w') as log_file:
-            try:
-                logger.info(
-                    f'Running `{prog} {" ".join(prog_args)}` in {self.flow_run_dir}')
-                with subprocess.Popen([prog, *prog_args],
-                                      cwd=self.flow_run_dir,
-                                      shell=False,
-                                      stdout=subprocess.PIPE if redirect_std else None,
-                                      bufsize=1,
-                                      universal_newlines=True,
-                                      encoding='utf-8',
-                                      errors='replace'
-                                      ) as proc:
-                    logger.info(
-                        f'Started {proc.args[0]}[{proc.pid}].{(" Standard output is logged to: " + str(stdout_logfile)) if redirect_std else ""}')
-
-                    def end_step():
-                        if spinner:
-                            if unicode:
-                                print('\r✅', end='')
-                            spinner.finish()
-                    if redirect_std:
-                        if initial_step:
-                            spinner = make_spinner(initial_step)
-                        while True:
-                            line = proc.stdout.readline()
-                            if not line:
-                                end_step()
-                                break
-                            log_file.write(line)
-                            log_file.flush()
-                            if verbose or echo_instructed:
-                                if disable_echo_re.match(line):
-                                    echo_instructed = False
-                                else:
-                                    print(line, end='')
-                            else:
-                                if not self.args.quiet:
-                                    if error_msg_re.match(line) or critwarn_msg_re.match(line):
-                                        if spinner:
-                                            print()
-                                        logger.error(line)
-                                    elif warn_msg_re.match(line):
-                                        if spinner:
-                                            print()
-                                        logger.warning(line)
-                                    elif enable_echo_re.match(line):
-                                        if not self.args.quiet:
-                                            echo_instructed = True
-                                        end_step()
-                                    else:
-                                        match = start_step_re.match(line)
-                                        if match:
-                                            end_step()
-                                            step = match.group('step')
-                                            spinner = make_spinner(step)
-                                        else:
-                                            if spinner:
-                                                spinner.next()
-
-            except FileNotFoundError as e:
-                self.fatal(
-                    f"Cannot execute `{prog}`. Make sure it's properly installed and is in the current PATH")
-            except KeyboardInterrupt as e:
-                if spinner:
-                    print(SHOW_CURSOR)
-                final_kill(proc)
-            finally:
-                final_kill(proc)
-
-        if spinner:
-            print(SHOW_CURSOR)
-
-        if proc.returncode != 0:
-            m = f'`{proc.args[0]}` exited with returncode {proc.returncode}'
-            logger.critical(
-                f'{m}. Please check `{stdout_logfile}` for error messages!')
-            if check:
-                raise NonZeroExit(m)
-        else:
-            logger.info(
-                f'Execution of {prog} in {self.flow_run_dir} completed with returncode {proc.returncode}')
-
-    def parse_report_regex(self, reportfile_path, re_pattern, *other_re_patterns, dotall=True):
-        # TODO fix debug and verbosity levels!
-        high_debug = self.args.verbose
-        if not reportfile_path.exists():
-            logger.warning(
-                f'File {reportfile_path} does not exist! Most probably the flow run had failed.\n Please check log files in {self.flow_run_dir}'
-            )
-            return False
-        with open(reportfile_path) as rpt_file:
-            content = rpt_file.read()
-
-            flags = re.MULTILINE | re.IGNORECASE
-            if dotall:
-                flags |= re.DOTALL
-
-            def match_pattern(pat):
-                match = re.search(pat, content, flags)
-                if match is None:
-                    return False
-                match_dict = match.groupdict()
-                for k, v in match_dict.items():
-                    self.results[k] = try_convert(v)
-                    logger.debug(f'{k}: {self.results[k]}')
-                return True
-
-            for pat in [re_pattern, *other_re_patterns]:
-                matched = False
-                if isinstance(pat, list):
-                    if high_debug:
-                        logger.debug(f"Matching any of: {pat}")
-                    for subpat in pat:
-                        matched = match_pattern(subpat)
-                        if matched:
-                            if high_debug:
-                                logger.debug("subpat matched!")
-                            break
-                else:
-                    if high_debug:
-                        logger.debug(f"Matching: {pat}")
-                    matched = match_pattern(pat)
-
-                if not matched:
-                    self.fatal(
-                        f"Error parsing report file: {rpt_file.name}\n Pattern not matched: {pat}\n")
-        return True
-
-    def print_results(self, results=None):
-        if not results:
-            results = self.results
-            # init to print_results time:
-            if results.get('runtime_minutes') is None:
-                results['runtime_minutes'] = (
-                    time.monotonic() - self.init_time) / 60
-        data_width = 32
-        name_width = 80 - data_width
-        hline = "-"*(name_width + data_width)
-
-        my_print("\n" + hline)
-        my_print(f"{'Results':^{name_width + data_width}s}")
-        my_print(hline)
-        for k, v in results.items():
-            if v is not None and not k.startswith('_'):
-                if isinstance(v, float):
-                    my_print(f'{k:{name_width}}{v:{data_width}.3f}')
-                elif isinstance(v, bool):
-                    bdisp = (colored.fg(
-                        "green") + "✓" if v else colored.fg("red") + "✗") + colored.attr("reset")
-                    my_print(f'{k:{name_width}}{bdisp:>{data_width}}')
-                elif isinstance(v, int):
-                    my_print(f'{k:{name_width}}{v:>{data_width}}')
-                elif isinstance(v, list):
-                    my_print(
-                        f'{k:{name_width}}{" ".join([str(x) for x in v]):<{data_width}}')
-                else:
-                    my_print(f'{k:{name_width}}{str(v):>{data_width}s}')
-        my_print(hline + "\n")
-
-    def dump_json(self, data, path: Path):
-        if path.exists():
-            modifiedTime = os.path.getmtime(path)
-            suffix = datetime.fromtimestamp(
-                modifiedTime).strftime("%Y-%m-%d-%H%M%S")
-            backup_path = path.with_suffix(f".backup_{suffix}.json")
-            logger.warning(
-                f"File already exists! Backing-up existing file to {backup_path}")
-            os.rename(path, backup_path)
-
-        with open(path, 'w') as outfile:
-            json.dump(data, outfile, default=lambda x: x.__dict__ if hasattr(
-                x, '__dict__') else str(x), indent=4)
-
-    def dump_results(self):
-        path = self.flow_run_dir / f'results.json'
-        self.dump_json(self.results, path)
-        logger.info(f"Results written to {path}")
-
-
-class SimFlow(Flow):
-    required_settings = {}
-
-    @property
-    def sim_sources(self):
-        tb_settings = self.settings.design["tb"]
-        srcs = self.settings.design["rtl"]['sources']
-        for src in tb_settings['sources']:
-            if not src in srcs:
-                srcs.append(src)
-        return srcs
-
-    @property
-    def sim_tops(self) -> List[str]:
-        """ a view of tb.top that returns a list of primary_unit [secondary_unit] """
-        # TODO is there ever a >= ternary_unit? If not switch to primary_unit, secondary_unit instead of the sim_tops list
-        tb_settings = self.settings.design["tb"]
-        tops = copy.deepcopy(tb_settings['top'])
-        if not isinstance(tops, list):
-            tops = [tops]
-        configuration_specification = tb_settings.get(
-            'configuration_specification')
-        if configuration_specification:
-            tops[0] = configuration_specification
-        return tops
-
-    @property
-    def tb_top(self) -> str:
-        top = self.settings.design["tb"]['top']
-        if isinstance(top, list):
-            top = top[0]
-        return top
-
-    def parse_reports(self):
-        self.results['success'] = True
-
-    @property
-    def vcd(self) -> str:
-        vcd = self.settings.flow.get('vcd')
-        if vcd:
-            if not isinstance(vcd, str): # e.g. True
-                vcd = 'dump.vcd'
-            elif not vcd.endswith('.vcd'):
-                vcd += '.vcd'
-        elif self.args.debug >= DebugLevel.LOW:
-            vcd = 'debug_dump.vcd'
-        return vcd
-
-
-class SynthFlow(Flow):
-    required_settings = {'clock_period': float}
-
-    # TODO FIXME set in plugin or elsewhere!!!
-    default_settings = {'allow_dsps': False, 'allow_brams': False}
-
-    def __init__(self, settings: Settings, args: SimpleNamespace, completed_dependencies: List['Flow']):
-        if not isinstance(self, SimFlow):
-            settings.design['tb'] = {}
-        super().__init__(settings, args, completed_dependencies)
-
-
-class DseFlow(Flow):
-    pass
-
-
-class FileResource:
-    @classmethod
-    def is_file_resource(cls, src):
-        return isinstance(src, cls) or (isinstance(src, dict) and 'file' in src)
-
-    def __init__(self, path) -> None:
-        self.file = None
-        self.hash = None
-
-        try:
-            # path must be absolute
-            self.file = Path(path).resolve(strict=True)
-        except Exception as e:
-            logger.critical(f"Design source file '{path}' does not exist!")
-            raise e
-
-        with open(self.file, 'rb') as f:
-            self.hash = hashlib.sha256(f.read()).hexdigest()
-
-    def __eq__(self, other):
-        # path is already absolute
-        return self.hash == other.hash and self.file.samefile(other.file)
-
-    def __hash__(self):
-        # path is already absolute
-        return hash(tuple(self.hash, str(self.file)))
-
-    def __str__(self):
-        return str(self.file)
-
-    def __repr__(self) -> str:
-        return 'FileResource:' + self.__str__()
-
-
-class DesignSource(FileResource):
-    @classmethod
-    def is_design_source(cls, src):
-        return cls.is_file_resource(src)
-
-    def __init__(self, file: str, type: str = None, standard: str = None, variant: str = None) -> None:
-
-        super().__init__(file)
-
-        def type_from_suffix(file: Path) -> str:
-            type_variants_map = {
-                ('vhdl', variant): ['vhd', 'vhdl'],
-                ('verilog', variant): ['v'],
-                ('verilog', 'systemverilog'): ['sv'],
-                ('bsv', variant): ['bsv'],
-                ('bs', variant): ['bs'],
-            }
-            for h, suffixes in type_variants_map.items():
-                if file.suffix[1:] in suffixes:
-                    return h
-            return None, None
-
-        self.type, self.variant = (
-            type, variant) if type else type_from_suffix(self.file)
-        self.standard = standard
diff --git a/xeda/flows/ghdl/__init__.py b/xeda/flows/ghdl/__init__.py
deleted file mode 100644
index 14d495aa..00000000
--- a/xeda/flows/ghdl/__init__.py
+++ /dev/null
@@ -1,104 +0,0 @@
-# © 2020 [Kamyar Mohajerani](mailto:kamyar@ieee.org)
-
-from ..flow import SimFlow, Flow
-
-class Ghdl(Flow):
-    pass
-
-class GhdlSim(Ghdl, SimFlow):
-
-    def run(self):
-        design_settings = self.settings.design
-        flow_settings = self.settings.flow
-        tb_settings = design_settings['tb']
-        vhdl_settings = design_settings['language']['vhdl']
-
-        # TODO synthesis, lint
-
-        warns = ['-Wbinding', '-Wreserved', '-Wlibrary', '-Wvital-generic',
-                 '-Wdelayed-checks', '-Wbody', '-Wspecs', '-Wunused', '--warn-no-runtime-error']
-
-        vhdl_std_opts = []
-        vhdl_std = vhdl_settings.get('standard')
-        if vhdl_std:
-            vhdl_std_opts.append(f'--std={"93c" if str(vhdl_std) == "93" else vhdl_std}')
-
-        optimize = ['-O3']
-
-        analysis_options = []
-
-        if vhdl_settings.get('synopsys'):
-            analysis_options.append('--ieee=synopsys')
-            vhdl_std_opts.append('-fsynopsys')
-
-        analysis_options += vhdl_std_opts + ['-frelaxed-rules', '--warn-no-vital-generic',
-                            '-frelaxed', '--mb-comments'] + optimize
-
-        run_options = ['--ieee-asserts=disable-at-0']  # TODO
-        elab_options = vhdl_std_opts + ['--syn-binding', '-frelaxed']
-
-
-        lib_paths = flow_settings.get('lib_paths', [])
-        if isinstance(lib_paths, str):
-            lib_paths = [lib_paths]
-        lib_paths = [f'-P{p}' for p in lib_paths]
-
-        if self.args.verbose:
-            analysis_options.append('-v')
-            elab_options.append('-v')
-
-        stop_time = self.settings.flow.get('stop_time')
-        if stop_time:
-            run_options.append(f'--stop-time={stop_time}')
-
-        # --sdf=min=PATH=FILENAME
-        # --sdf=typ=PATH=FILENAME
-        # --sdf=max=PATH=FILENAME
-        tb_uut = tb_settings.get('uut')
-        sdf = self.settings.flow.get('sdf')
-        if sdf:
-            if not isinstance(sdf, list):
-                sdf = [sdf]
-            for s in sdf:
-                if isinstance(s, str):
-                    s = {"file": s}
-                root = s.get("root", tb_uut)
-                assert root, "neither SDF root nor tb.uut are provided"
-                run_options.append(f'--sdf={s.get("delay", "max")}={root}={s["file"]}')
-
-        ghw = flow_settings.get('wave', flow_settings.get('ghw'))
-
-        if self.vcd:
-            run_options.append(f'--vcd={self.vcd}')
-        elif ghw:
-            if not isinstance(ghw, str):
-                ghw = 'dump.ghw'
-            if not ghw.endswith('.ghw'):
-                ghw += '.ghw'
-            run_options.append(f'--wave={ghw}')
-
-        tb_generics_opts = [f"-g{k}={v}" for k, v in tb_settings.get("generics", {}).items()]
-        
-        self.run_process('ghdl', ['remove'] + vhdl_std_opts,
-                         initial_step='Clean up previously-generated files and library',
-                         stdout_logfile='ghdl_remove_stdout.log',
-                         check=True
-                         )
-
-        self.run_process('ghdl', ['import'] + analysis_options + warns + list(map(lambda x: str(x), self.sim_sources)),
-                         initial_step='Analyzing VHDL files',
-                         stdout_logfile='ghdl_analyze_stdout.log',
-                         check=True
-                         )
-
-        self.run_process('ghdl', ['make'] + elab_options + optimize + warns + lib_paths + self.sim_tops,
-                         initial_step='Elaborating design',
-                         stdout_logfile='ghdl_elaborate_stdout.log',
-                         check=True
-                         )
-
-        self.run_process('ghdl', ['run'] + vhdl_std_opts + self.sim_tops + run_options + tb_generics_opts, # GHDL supports primary_unit [secondary_unit] 
-                         initial_step='Running simulation',
-                         stdout_logfile='ghdl_run_stdout.log',
-                         force_echo=True
-                         )
diff --git a/xeda/flows/modelsim/__init__.py b/xeda/flows/modelsim/__init__.py
deleted file mode 100644
index f0d5f133..00000000
--- a/xeda/flows/modelsim/__init__.py
+++ /dev/null
@@ -1,58 +0,0 @@
-# © 2020 [Kamyar Mohajerani](mailto:kamyar@ieee.org)
-
-import re
-from ..flow import SimFlow
-
-
-class Modelsim(SimFlow):
-
-    def run(self):
-        vcom_options = ['-lint']
-        vlog_options = ['-lint']
-        vsim_opts = []
-        rtl_settings = self.settings.design['rtl']
-        tb_settings = self.settings.design['tb']
-        flow_settings = self.settings.flow
-        sdf_conf = flow_settings.get('sdf')
-        tb_uut = tb_settings.get('uut')
-        libraries = flow_settings.get('libraries')
-        if libraries:
-            vsim_opts.extend([f'-L {l}' for l in libraries ])
-        sim_top = tb_settings['top']
-        if isinstance(sim_top, list):
-            sim_top = ' '.join(sim_top)
-        # sdf has two fields: file (path to sdf file), and type [min, max, typ]
-        if sdf_conf and tb_uut:
-            if not isinstance(sdf_conf, list):
-                sdf_conf = [sdf_conf]
-            for sdf in sdf_conf:
-                if isinstance(sdf, str):
-                    sdf = {'file': sdf, 'delay_type': 'max'}
-                vsim_opts.append(f'-sdf{sdf["delay_type"]}')
-                vsim_opts.append(f'{tb_uut}={sdf["file"]}')
-            if not self.vcd:
-                flow_settings['vcd'] = 'timing_sim.vcd'
-
-        tb_generics_opts = ' '.join([f"-g{k}={v}" for k, v in tb_settings["generics"].items()])
-
-        modelsimini = flow_settings.get('modelsimini')
-
-
-        script_path = self.copy_from_template(f'run.tcl',
-                                              generics_options=tb_generics_opts,
-                                              vcom_opts=' '.join(vcom_options),
-                                              vlog_opts=' '.join(vlog_options),
-                                              vsim_opts=' '.join(vsim_opts),
-                                              top=sim_top,
-                                              vcd=self.vcd
-                                              )
-
-        modelsim_opts = ['-batch', '-do', f'do {script_path}']
-
-        if modelsimini:
-            modelsim_opts.extend(['-modelsimini', modelsimini])
-        
-        self.run_process('vsim', modelsim_opts,
-                         stdout_logfile='modelsim_stdout.log',
-                         check=True
-                         )
diff --git a/xeda/flows/quartus/__init__.py b/xeda/flows/quartus/__init__.py
deleted file mode 100644
index 2243ebad..00000000
--- a/xeda/flows/quartus/__init__.py
+++ /dev/null
@@ -1,325 +0,0 @@
-# © 2020 [Kamyar Mohajerani](mailto:kamyar@ieee.org)
-
-from ..flow import DseFlow, Flow, SimFlow, SynthFlow
-from ...utils import parse_csv
-
-
-class Quartus(Flow):
-    required_settings = {'clock_period': float, 'fpga_part': str}
-
-    def create_project(self, **kwargs):
-
-        strategy_settings = {
-            'Timing': {
-                # see https://www.intel.com/content/www/us/en/programmable/documentation/zpr1513988353912.html
-                # https://www.intel.com/content/www/us/en/programmable/quartushelp/current/index.htm
-                # BALANCED "HIGH PERFORMANCE EFFORT" AGGRESSIVE PERFORMANCE
-                # "High Performance with Maximum Placement Effort"
-                # "Superior Performance"
-                # "Superior Performance with Maximum Placement Effort"
-                # "Aggressive Area"
-                # "High Placement Routability Effort"
-                # "High Packing Routability Effort"
-                # "Optimize Netlist for Routability
-                # "High Power Effort"
-                "OPTIMIZATION_MODE": "HIGH PERFORMANCE EFFORT",
-                "REMOVE_REDUNDANT_LOGIC_CELLS": "ON",
-                "AUTO_RESOURCE_SHARING": "ON",
-                "ALLOW_REGISTER_RETIMING": "ON",
-
-                "SYNTH_GATED_CLOCK_CONVERSION": "ON",
-
-
-                # faster: AUTO FIT, fastest: FAST_FIT
-                "FITTER_EFFORT": "STANDARD FIT",
-
-                # AREA, SPEED, BALANCED
-                "STRATIX_OPTIMIZATION_TECHNIQUE": "SPEED",
-                "CYCLONE_OPTIMIZATION_TECHNIQUE": "SPEED",
-
-                # see https://www.intel.com/content/www/us/en/programmable/documentation/rbb1513988527943.html
-                # The Router Effort Multiplier controls how quickly the router tries to find a valid solution. The default value is 1.0 and legal values must be greater than 0.
-                # Numbers higher than 1 help designs that are difficult to route by increasing the routing effort.
-                # Numbers closer to 0 (for example, 0.1) can reduce router runtime, but usually reduce routing quality slightly.
-                # Experimental evidence shows that a multiplier of 3.0 reduces overall wire usage by approximately 2%. Using a Router Effort Multiplier higher than the default value can benefit designs with complex datapaths with more than five levels of logic. However, congestion in a design is primarily due to placement, and increasing the Router Effort Multiplier does not necessarily reduce congestion.
-                # Note: Any Router Effort Multiplier value greater than 4 only increases by 10% for every additional 1. For example, a value of 10 is actually 4.6.
-                "PLACEMENT_EFFORT_MULTIPLIER": 3.0,
-                "ROUTER_EFFORT_MULTIPLIER": 3.0,
-
-                # NORMAL, MINIMUM,MAXIMUM
-                "ROUTER_TIMING_OPTIMIZATION_LEVEL": "MAXIMUM",
-
-                # ALWAYS, AUTOMATICALLY, NEVER
-                "FINAL_PLACEMENT_OPTIMIZATION": "ALWAYS",
-                # "PHYSICAL_SYNTHESIS_COMBO_LOGIC_FOR_AREA": "ON",
-                # ?
-                # "ADV_NETLIST_OPT_SYNTH_GATE_RETIME": "ON",
-                # ?
-                # "ADV_NETLIST_OPT_SYNTH_WYSIWYG_REMAP": "ON",
-
-                "AUTO_PACKED_REGISTERS_STRATIX": "OFF",
-                "AUTO_PACKED_REGISTERS_CYCLONE": "OFF",
-                "PHYSICAL_SYNTHESIS_COMBO_LOGIC": "ON",
-                "PHYSICAL_SYNTHESIS_REGISTER_DUPLICATION": "ON",
-                "PHYSICAL_SYNTHESIS_REGISTER_RETIMING": "ON",
-                "PHYSICAL_SYNTHESIS_EFFORT": "EXTRA",
-
-                #NORMAL, OFF, EXTRA_EFFORT
-                # "OPTIMIZE_POWER_DURING_SYNTHESIS": "NORMAL",
-
-                # Used during placement. Use of a higher value increases compilation time, but may increase the quality of placement.
-                "INNER_NUM": 8,
-                # SYNTH_CRITICAL_CLOCK: ON, OFF : Speed Optimization Technique for Clock Domains}
-            }, 'Default' :{
-
-            }
-        }
-
-        project_settings = None
-        if 'project_settings' in self.settings.flow:
-            project_settings = self.settings.flow['project_settings']
-
-        strategy = self.settings.flow.get('strategy', 'Default')
-        # TODO manage settings
-        if not project_settings:
-            project_settings = strategy_settings[strategy]
-
-        # ???? TODO not sure this is right, maybe apply ramstyle/dspstyle on all hierarchy using TCL?
-        if not self.settings.flow['allow_dsps']:
-            project_settings["AUTO_DSP_RECOGNITION"] = "OFF"
-        if not self.settings.flow['allow_brams']:
-            project_settings["AUTO_RAM_RECOGNITION"] = "OFF"
-            project_settings["AUTO_ROM_RECOGNITION"] = "OFF"
-
-        clock_sdc_path = self.copy_from_template(f'clock.sdc')
-        script_path = self.copy_from_template(
-            f'create_project.tcl',
-            sdc_files=[clock_sdc_path],
-            project_settings=project_settings,
-            **kwargs
-        )
-        self.run_process('quartus_sh', ['-t', str(script_path)], stdout_logfile='create_project_stdout.log')
-
-        # self.run_process('quartus_sh',
-        #                  ['--dse', '-project', self.settings.design['name'], '-nogui', '-concurrent-compiles', '8', '-exploration-space',
-        #                   "Extra Effort Space", '-optimization-goal', "Optimize for Speed", '-report-all-resource-usage', '-ignore-failed-base'],
-        #                  stdout_logfile='dse_stdout.log'
-        #                  )
-
-    # def __init__(self, settings, args, logger):
-    #     # def supported_quartus_generic(k, v, sim):
-    #     #     if sim:
-    #     #         return True
-    #     #     if isinstance(v, int):
-    #     #         return True
-    #     #     if isinstance(v, bool):
-    #     #         return True
-    #     #     v = str(v)
-    #     #     return (v.isnumeric() or (v.strip().lower() in {'true', 'false'}))
-
-    #     # def quartus_gen_convert(k, x, sim):
-    #     #     if sim:
-    #     #         if isinstance(x, dict) and "file" in x:
-    #     #             p = x["file"]
-    #     #             assert isinstance(p, str), "value of `file` should be a relative or absolute path string"
-    #     #             x = self.conv_to_relative_path(p.strip())
-    #     #             self.logger.info(f'Converting generic `{k}` marked as `file`: {p} -> {x}')
-    #     #     xl = str(x).strip().lower()
-    #     #     if xl == 'false':
-    #     #         return "1\\'b0"
-    #     #     if xl == 'true':
-    #     #         return "1\\'b1"
-    #     #     return x
-
-    #     # def quartus_generics(kvdict, sim):
-    #     #     return ' '.join([f"-generic {k}={quartus_gen_convert(k, v, sim)}" for k, v in kvdict.items() if supported_quartus_generic(k, v, sim)])
-
-    #     super().__init__(settings, args, logger)
-
-    #     # self.settings.flow['generics_options'] = quartus_generics(self.settings.design["generics"], sim=False)
-    #     # self.settings.flow['tb_generics_options'] = quartus_generics(self.settings.design["tb_generics"], sim=True)
-
-
-class QuartusSynth(Quartus, SynthFlow):
-
-    def run(self):
-        prj_name = self.settings.design['name']
-        self.create_project()
-        script_path = self.copy_from_template(f'compile.tcl')
-        self.run_process('quartus_sh',
-                         ['-t', str(script_path)],
-                         stdout_logfile='compile_stdout.log'
-                         )
-        # self.run_process('quartus_eda', [prj_name, '--simulation', '--functional', '--tool=modelsim_oem', '--format=verilog'],
-        #                         stdout_logfile='eda_1_stdout.log'
-        #                         )
-
-    def parse_reports(self):
-        failed = False
-
-        resources = parse_csv(
-            self.reports_dir / 'Fitter' / 'Resource_Section' / 'Fitter_Resource_Utilization_by_Entity.csv',
-            id_field='Compilation Hierarchy Node',
-            field_parser=lambda s: int(s.split()[0]),
-            id_parser=lambda s: s.strip()[1:],
-            interesting_fields=['Logic Cells', 'Memory Bits', 'M9Ks', 'DSP Elements',
-                                'LUT-Only LCs',	'Register-Only LCs', 'LUT/Register LCs']
-        )
-
-        rtl_top = self.settings.design['rtl']['top']
-
-        top_resources = resources[rtl_top]
-
-        top_resources['lut'] = top_resources['LUT-Only LCs'] + top_resources['LUT/Register LCs']
-        top_resources['ff'] = top_resources['Register-Only LCs'] + top_resources['LUT/Register LCs']
-
-        self.results.update(top_resources)
-
-        # TODO is this the most reliable timing report?
-        slacks = parse_csv(
-            self.reports_dir / 'Timing_Analyzer' / 'Multicorner_Timing_Analysis_Summary.csv',
-            id_field='Clock',
-            field_parser=lambda s: float(s.strip()),
-            id_parser=lambda s: s.strip(),
-            interesting_fields=['Setup', 'Hold']
-        )
-        worst_slacks = slacks['Worst-case Slack']
-        wns = worst_slacks['Setup']
-        whs = worst_slacks['Hold']
-        self.results['wns'] = wns
-        self.results['whs'] = whs
-        self.results['clock_period'] = float(self.settings.flow['clock_period'])
-        self.results['clock_frequency'] = 1000 / self.results['clock_period']
-        
-
-        failed |= wns < 0 or whs < 0
-
-        vcc = '1200mV'
-        corner = 'Slow'
-        for temp in ['85C', '0C']:
-            fmax = parse_csv(
-                self.reports_dir / 'Timing_Analyzer' /
-                f'{corner}_{vcc}_{temp}_Model' / f'{corner}_{vcc}_{temp}_Model_Fmax_Summary.csv',
-                id_field='Clock Name',
-                field_parser=lambda s: s.strip().split(),
-                id_parser=lambda s: s.strip(),
-                interesting_fields=['Fmax']
-            )
-            self.results[f'fmax_{temp}'] = fmax['clock']['Fmax']
-
-        temp = '85C'
-        self.results['clock_frequency'] = self.results[f'fmax_{temp}']
-
-        self.results['success'] = not failed
-
-
-# class QuartusPower(QuartusSynth, SimFlow):
-#     def run(self):
-#         self.create_project(vcd=self.settings.flow.get('vcd'))
-#         script_path = self.copy_from_template(f'compile.tcl')
-#         self.run_process('quartus_sh',
-#                          ['-t', str(script_path)],
-#                          stdout_logfile='compile_stdout.log'
-#                          )
-
-#     def parse_reports(self):
-#         failed = False
-
-#         resources = parse_csv(
-#             self.reports_dir / 'Fitter' / 'Resource_Section' / 'Fitter_Resource_Utilization_by_Entity.csv',
-#             id_field='Compilation Hierarchy Node',
-#             field_parser=lambda s: int(s.split()[0]),
-#             id_parser=lambda s: s.strip()[1:],
-#             interesting_fields=['Logic Cells', 'Memory Bits', 'M9Ks', 'DSP Elements',
-#                                 'LUT-Only LCs',	'Register-Only LCs', 'LUT/Register LCs']
-#         )
-
-#         top_resources = resources[self.settings.design['top']]
-
-#         top_resources['lut'] = top_resources['LUT-Only LCs'] + top_resources['LUT/Register LCs']
-#         top_resources['ff'] = top_resources['Register-Only LCs'] + top_resources['LUT/Register LCs']
-
-#         self.results.update(top_resources)
-
-#         # TODO is this the most reliable timing report?
-#         slacks = parse_csv(
-#             self.reports_dir / 'Timing_Analyzer' / 'Multicorner_Timing_Analysis_Summary.csv',
-#             id_field='Clock',
-#             field_parser=lambda s: float(s.strip()),
-#             id_parser=lambda s: s.strip(),
-#             interesting_fields=['Setup', 'Hold']
-#         )
-#         worst_slacks = slacks['Worst-case Slack']
-#         wns = worst_slacks['Setup']
-#         whs = worst_slacks['Hold']
-#         self.results['wns'] = wns
-#         self.results['whs'] = whs
-
-#         failed |= wns < 0 or whs < 0
-
-#         for temp in ['85C', '0C']:
-#             fmax = parse_csv(
-#                 self.reports_dir / 'Timing_Analyzer' /
-#                 f'Slow_1200mV_{temp}_Model' / f'Slow_1200mV_{temp}_Model_Fmax_Summary.csv',
-#                 id_field='Clock Name',
-#                 field_parser=lambda s: s.strip().split(),
-#                 id_parser=lambda s: s.strip(),
-#                 interesting_fields=['Fmax']
-#             )
-#             self.results[f'fmax_{temp}'] = fmax['clock']['Fmax']
-
-#         self.results['success'] = not failed
-
-
-# class QuartusDse(QuartusSynth, DseFlow):
-#     def run(self):
-#         self.create_project()
-#         # 'explore': Exploration flow to use, if not specified in --config
-#         #   configuration file. Valid flows: timing_aggressive,
-#         #   all_optimization_modes, timing_high_effort, seed,
-#         #   area_aggressive, power_high_effort, power_aggressive
-#         # 'compile_flow':  'full_compile', 'fit_sta' and 'fit_sta_asm'.
-#         # 'timeout': Limit the amount of time a compute node is allowed to run. Format: hh:mm:ss
-#         if 'dse' not in self.settings.flow:
-#             self.fatal('`flows.quartus.dse` settings are missing!')
-
-#         dse = self.settings.flow['dse']
-#         if 'nproc' not in dse or not dse['nproc']:
-#             dse['nproc'] = self.nthreads
-
-#         script_path = self.copy_from_template(f'settings.dse',
-#                                               dse=dse
-#                                               )
-#         self.run_process('quartus_dse',
-#                          ['--use-dse-file', script_path, self.settings.design['name']],
-#                          stdout_logfile='dse_stdout.log',
-#                          initial_step="Running Quartus DSE",
-#                          )
-
-#     def parse_reports(self):
-#         'quartus_dse_report.json'
-#         pass
-
-
-# DES:
-
-# Available exploration spaces for this family are:
-# "Seed Sweep"
-# "Extra Effort Space"
-# "Extra Effort Space for Quartus Prime Integrated Synthesis Projects"
-# "Area Optimization Space"
-# "Signature: Placement Effort Multiplier"
-# "Custom Space"
-
-# Valid optimization-goal options are:
-# "Optimize for Speed"
-# "Optimize for Area"
-# "Optimize for Power"
-# "Optimize for Negative Slack and Failing Paths"
-# "Optimize for Average Period"
-# "Optimize for Quality of Fit"
-
-
-# -run-power ?
-
-#
diff --git a/xeda/flows/quartus/templates/clock.sdc b/xeda/flows/quartus/templates/clock.sdc
deleted file mode 100644
index 6b6d2276..00000000
--- a/xeda/flows/quartus/templates/clock.sdc
+++ /dev/null
@@ -1,4 +0,0 @@
-create_clock -period {{ "%.3f"|format(flow.clock_period) }} -name clock [get_ports {{design.rtl.clock_port}}]
-
-derive_pll_clocks
-derive_clock_uncertainty
\ No newline at end of file
diff --git a/xeda/flows/quartus/templates/create_project.tcl b/xeda/flows/quartus/templates/create_project.tcl
deleted file mode 100644
index 33df8202..00000000
--- a/xeda/flows/quartus/templates/create_project.tcl
+++ /dev/null
@@ -1,45 +0,0 @@
-set design_name           {{design.name}}
-set top                   {{design.rtl.top}}
-{% if debug %}
-foreach key [array names quartus] {
-    puts "${key}=$quartus($key)"
-}
-{% endif %}
-
-package require ::quartus::project
-
-puts "\n===========================( Setting up project and settings )==========================="
-project_new ${design_name} -overwrite
-
-set_global_assignment -name NUM_PARALLEL_PROCESSORS {{nthreads}}
-
-{% if flow.fpga_part.startswith("10CL0") %}
-set_global_assignment -name FAMILY "Cyclone 10 LP"
-{% endif %}
-set_global_assignment -name DEVICE {{flow.fpga_part}}
-
-set_global_assignment -name TOP_LEVEL_ENTITY ${top}
-
-{% if design.language.vhdl.standard == "08" %}
-    set_global_assignment -name VHDL_INPUT_VERSION VHDL_2008
-{% endif %}
-
-{% for src in design.rtl.sources %}
-set_global_assignment -name {% if src.type == "verilog" and src.variant == "systemverilog" -%} SYSTEMVERILOG {%- else -%} {{src.type|upper}} {%- endif -%}_FILE {{src.file}}
-{% endfor %}
-
-{% for sdc_file in sdc_files %}
-set_global_assignment -name SDC_FILE {{sdc_file}}
-{% endfor %}
-
-puts "clocks: [get_clocks]"
-
-set_global_assignment -name NUM_PARALLEL_PROCESSORS {{nthreads}}
-
-{% for k,v in project_settings.items() %}
-set_global_assignment -name {{k}} {% if v is number -%} {{v}} {%- else -%} "{{v}}" {%- endif %}
-{% endfor %}
-
-set_global_assignment -name FLOW_ENABLE_POWER_ANALYZER ON
-
-project_close
\ No newline at end of file
diff --git a/xeda/flows/settings.py b/xeda/flows/settings.py
deleted file mode 100644
index 7aec25d7..00000000
--- a/xeda/flows/settings.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# © 2020 [Kamyar Mohajerani](mailto:kamyar@ieee.org)
-
-class Settings:
-    def __init__(self) -> None:
-        self.design = dict()
-        self.flow = dict()
-        self.project = dict()
\ No newline at end of file
diff --git a/xeda/flows/vivado/__init__.py b/xeda/flows/vivado/__init__.py
deleted file mode 100644
index a860457a..00000000
--- a/xeda/flows/vivado/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-# © 2020 [Kamyar Mohajerani](mailto:kamyar@ieee.org)
-
-from .vivado_sim import VivadoSim, VivadoPostsynthSim
-from .vivado_synth import VivadoSynth
-from .vivado_power import VivadoPower
-from .vivado_project import VivadoPrjSynth
\ No newline at end of file
diff --git a/xeda/flows/vivado/templates/clock.xdc b/xeda/flows/vivado/templates/clock.xdc
deleted file mode 100644
index f9d71697..00000000
--- a/xeda/flows/vivado/templates/clock.xdc
+++ /dev/null
@@ -1,8 +0,0 @@
-create_clock -period {{flow.clock_period|round(3,'floor')}} -name clock [get_ports {{design.rtl.clock_port}}]
-
-{% if constrain_io %}
-set_input_delay  -clock clock {{input_delay}} [filter [all_inputs] {NAME != {{design.rtl.clock_port}} } ]
-set_output_delay -clock clock {{output_delay}} [all_outputs]
-{% endif %}
-
-set_units -power mW
\ No newline at end of file
diff --git a/xeda/flows/vivado/templates/vivado_project.tcl b/xeda/flows/vivado/templates/vivado_project.tcl
deleted file mode 100644
index 6092fc23..00000000
--- a/xeda/flows/vivado/templates/vivado_project.tcl
+++ /dev/null
@@ -1,137 +0,0 @@
-# Auto-generated by Xeda
-
-create_project -part {{flow.fpga_part}} -force -verbose {{design.name}}
-
-# These settings are set by Xeda
-set design_name           {{design.name}}
-set vhdl_std              {{design.language.vhdl.standard}}
-set nthreads              {{nthreads}}
-
-set fail_critical_warning {{flow.fail_critical_warning}}
-set fail_timing           {{flow.fail_timing}}
-set bitstream             false
-
-set reports_dir           {{reports_dir}}
-set synth_output_dir      {{synth_output_dir}}
-set checkpoints_dir       {{checkpoints_dir}}
-
-{% include 'util.tcl' %}
-
-puts "Targeting device: {{flow.fpga_part}}"
-
-set_param general.maxThreads ${nthreads}
-
-file mkdir ${synth_output_dir}
-file mkdir ${reports_dir}
-file mkdir [file join ${reports_dir} post_synth]
-file mkdir [file join ${reports_dir} post_place]
-file mkdir [file join ${reports_dir} post_route]
-file mkdir ${checkpoints_dir}
-
-puts "\n================================( Read Design Files and Constraints )================================"
-
-{% for src in design.rtl.sources %}
-{%- if src.type == 'verilog' %}
-{%- if src.variant == 'systemverilog' %}
-puts "Reading SystemVerilog file {{src.file}}"
-if { [catch {eval read_verilog -sv {{src.file}} } myError]} {
-    errorExit $myError
-}
-{% else %}
-puts "Reading Verilog file {{src.file}}"
-if { [catch {eval read_verilog {{src.file}} } myError]} {
-    errorExit $myError
-}
-{%- endif %}
-{%- endif %}
-{% if src.type == 'vhdl' %}
-puts "Reading VHDL file {{src.file}}"
-if { [catch {eval read_vhdl {% if design.language.vhdl.standard == "08" %} -vhdl2008 {%- endif %} {{src.file}} } myError]} {
-    errorExit $myError
-}
-{%- endif %}
-{%- endfor %}
-
-# TODO: Skip saving some artifects in case timing not met or synthesis failed for any reason
-
-{% for xdc_file in xdc_files %}
-read_xdc {{xdc_file}}
-{% endfor %}
-
-set_property top {{design.rtl.top}} [get_fileset sources_1]
-
-
-{% for name,value in options.synth.items() %}
-set_property -name {{name}} -value {{value}} -objects [get_runs synth_1]
-{% endfor %}
-
-{% for name,value in options.impl.items() %}
-set_property -name {{name}} -value {{value}} -objects [get_runs impl_1]
-{% endfor %}
-
-puts "\n================================( Running Synthesis )================================="
-launch_runs synth_1 -jobs {{nthreads}}
-wait_on_run synth_1
-
-puts "\n================================( Running Implementation )================================="
-launch_runs impl_1 -jobs {{nthreads}}
-wait_on_run impl_1
-
-
-puts "\n=============================( Opening Implemented Design )=============================="
-open_run impl_1
-
-
-puts "\n=============================( Writing Checkpoint )=============================="
-write_checkpoint -force ${checkpoints_dir}/post_route
-
-puts "\n==============================( Writing Reports )================================"
-report_timing_summary -check_timing_verbose -no_header -report_unconstrained -path_type full -input_pins -max_paths 10 -delay_type min_max -file ${reports_dir}/post_route/timing_summary.rpt
-report_timing  -no_header -input_pins  -unique_pins -sort_by group -max_paths 100 -path_type full -delay_type min_max -file ${reports_dir}/post_route/timing.rpt
-reportCriticalPaths ${reports_dir}/post_route/critpath_report.csv
-## report_clock_utilization                                        -force -file ${reports_dir}/post_route/clock_utilization.rpt
-report_utilization                                              -force -file ${reports_dir}/post_route/utilization.rpt
-report_utilization                                              -force -file ${reports_dir}/post_route/utilization.xml -format xml
-report_utilization -hierarchical                                -force -file ${reports_dir}/post_route/hierarchical_utilization.xml -format xml
-report_utilization -hierarchical                                -force -file ${reports_dir}/post_route/hierarchical_utilization.rpt
-## report_utilization -hierarchical                                -force -file ${reports_dir}/post_route/hierarchical_utilization.xml -format xml
-report_power                                                    -file ${reports_dir}/post_route/power.rpt
-report_drc                                                      -file ${reports_dir}/post_route/drc.rpt
-## report_ram_utilization                                          -file ${reports_dir}/post_route/ram_utilization.rpt -append
-report_methodology                                              -file ${reports_dir}/post_route/methodology.rpt
-
-set timing_slack [get_property SLACK [get_timing_paths]]
-puts "Final timing slack: $timing_slack ns"
-
-report_qor_suggestions -file ${reports_dir}/post_route/qor_suggestions.rpt 
-# -max_strategies 5
-# write_qor_suggestions -force qor_suggestions.rqs
-
-# close_project
-
-if {$timing_slack < 0} {
-    puts "\n===========================( *ENABLE ECHO* )==========================="
-    puts "ERROR: Failed to meet timing by $timing_slack, see [file join ${reports_dir} post_route timing_summary.rpt] for details"
-    if {$fail_timing} {
-        exit 1
-    }
-    puts "\n===========================( *DISABLE ECHO* )==========================="
-} else {
-    puts "\n==========================( Writing Netlist and SDF )============================="
-    write_sdf -mode timesim -process_corner slow -force -file ${synth_output_dir}/impl_timesim.sdf
-    # should match sdf
-    write_verilog -mode timesim -sdf_anno false -force -file ${synth_output_dir}/impl_timesim.v
-##    write_verilog -mode timesim -sdf_anno false -include_xilinx_libs -write_all_overrides -force -file ${synth_output_dir}/impl_timesim_inlined.v
-##    write_verilog -mode funcsim -force ${synth_output_dir}/impl_funcsim_noxlib.v
-##    write_vhdl    -mode funcsim -include_xilinx_libs -write_all_overrides -force -file ${synth_output_dir}/impl_funcsim.vhd
-    write_xdc -no_fixed_only -force ${synth_output_dir}/impl.xdc
-
-    if {${bitstream}} {
-        puts "\n==============================( Writing Bitstream )==============================="
-        write_bitstream -force ${synth_output_dir}/bitstream.bit
-    }
-    showWarningsAndErrors
-}
-
-
-
diff --git a/xeda/flows/vivado/vivado.py b/xeda/flows/vivado/vivado.py
deleted file mode 100644
index 2b220604..00000000
--- a/xeda/flows/vivado/vivado.py
+++ /dev/null
@@ -1,82 +0,0 @@
-# © 2020 [Kamyar Mohajerani](mailto:kamyar@ieee.org)
-import html
-import logging
-from xeda.utils import try_convert
-from xml.etree import ElementTree
-from ..flow import Flow, DebugLevel
-from functools import reduce
-
-logger = logging.getLogger()
-
-
-def supported_vivado_generic(k, v, sim):
-    if sim:
-        return True
-    if isinstance(v, int):
-        return True
-    if isinstance(v, bool):
-        return True
-    v = str(v)
-    return (v.isnumeric() or (v.strip().lower() in {'true', 'false'}))
-
-
-def vivado_gen_convert(k, x, sim):
-    if sim:
-        return x
-    xl = str(x).strip().lower()
-    if xl == 'false':
-        return "1\\'b0"
-    if xl == 'true':
-        return "1\\'b1"
-    return x
-
-
-def vivado_generics(kvdict, sim):
-    return ' '.join([f"-generic{'_top' if sim else ''} {k}={vivado_gen_convert(k, v, sim)}" for k, v in kvdict.items() if supported_vivado_generic(k, v, sim)])
-
-
-class Vivado(Flow):
-    reports_subdir_name = 'reports'
-
-    def run_vivado(self, script_path, stdout_logfile=None):
-        if stdout_logfile is None:
-            stdout_logfile = f'{self.name}_stdout.log'
-        debug = self.args.debug > DebugLevel.NONE
-        vivado_args = ['-nojournal', '-mode', 'tcl' if debug >=
-                       DebugLevel.HIGHEST else 'batch', '-source', str(script_path)]
-        # if not debug:
-        #     vivado_args.append('-notrace')
-        return self.run_process('vivado', vivado_args, initial_step='Starting vivado',
-                                stdout_logfile=stdout_logfile)
-
-    @staticmethod
-    def parse_xml_report(report_xml):
-        tree = ElementTree.parse(report_xml)
-
-        data = {}
-        # components = {}
-
-        for section in tree.findall(f"./section"):
-            section_title = section.get("title")
-            for table in section.findall("./table"):
-                table_data = {}
-                header = [html.unescape(col.attrib['contents']).strip() for col in table.findall("./tablerow/tableheader")]
-                for tablerow in table.findall("./tablerow"):
-                    cells = [html.unescape(cell.attrib['contents']).strip() for cell in tablerow.findall("./tablecell")]
-                    if cells:
-                        # choose 0th element as "index data" (distinct key)
-                        cell_data = {h:c for h,c in zip(header[1:],cells[1:]) if c}
-                        cell_key = cells[0]
-                        if cell_data:
-                            table_data[cell_key] = try_convert(cell_data, to_str=False)
-                if table_data:
-                    table_title = table.get("title")
-                    title = section_title + ":" + table_title if table_title else section_title
-                    data[title] = table_data
-        return data
-        
-    @staticmethod
-    def get_from_path(dct: dict, path):
-        if isinstance(path, str):
-            path = path.split('.')
-        return reduce(dict.__getitem__, path, dct)
diff --git a/xeda/flows/vivado/vivado_power.py b/xeda/flows/vivado/vivado_power.py
deleted file mode 100644
index 0c99cf28..00000000
--- a/xeda/flows/vivado/vivado_power.py
+++ /dev/null
@@ -1,111 +0,0 @@
-# © 2021 [Kamyar Mohajerani](mailto:kamyar@ieee.org)
-
-import copy
-import logging
-import os
-import math
-import csv
-from pathlib import Path
-import re
-from types import SimpleNamespace
-from typing import List
-from xml.etree import ElementTree
-import html
-
-from ..flow import DesignSource, Flow
-from ...flows.settings import Settings
-from .vivado_sim import VivadoPostsynthSim
-from .vivado_synth import VivadoSynth
-from .vivado import Vivado
-
-logger = logging.getLogger()
-
-
-class VivadoPower(Vivado):
-
-    required_settings = {'clock_period'}
-
-    default_saif_file = 'impl_timing.saif'
-
-    @classmethod
-    def prerequisite_flows(cls, flow_settings, _design_settings):
-        synth_overrides = dict(strategy=flow_settings.get('strategy', 'AreaPower')) ## FIXME!!! For reasons still unknown, not all strategies lead to correct post-impl simulation
-        postsynthsim_overrides = dict(constrain_io=True, elab_debug='typical',
-                                      saif=cls.default_saif_file,  dependencies=dict(vivado_synth=synth_overrides))
-
-        period = flow_settings.get('clock_period')
-        if period:
-            postsynthsim_overrides['clock_period'] = period
-            synth_overrides['clock_period'] = period
-
-        opt_power = flow_settings.get('optimize_power')
-        if opt_power is not None:
-            postsynthsim_overrides['optimize_power'] = opt_power
-            synth_overrides['optimize_power'] = opt_power
-
-        return {VivadoPostsynthSim: (postsynthsim_overrides, {})}
-
-    def __init__(self, settings: Settings, args: SimpleNamespace, completed_dependencies: List['Flow']):
-        self.postsynthsim_flow = completed_dependencies[0]
-        self.postsynthsim_settings = self.postsynthsim_flow.settings.flow
-        self.synth_flow = self.postsynthsim_flow.completed_dependencies[0]
-        self.synth_settings = self.synth_flow.settings.flow
-        self.synth_results = self.synth_flow.results
-        self.power_report_filename = 'power_impl_timing.xml'
-        super().__init__(settings, args, completed_dependencies)
-
-    def run(self):
-        run_configs = self.postsynthsim_settings.get('run_configs')
-
-        def update_saif_path(rc):
-            rc['saif'] = str(self.postsynthsim_flow.flow_run_dir / rc['saif'])
-            return rc
-
-        if run_configs:
-            run_configs = [update_saif_path(rc) for rc in run_configs]
-        else:
-            run_configs = dict(saif=self.default_saif_file,
-                               report=self.power_report_filename)
-
-        self.run_configs = run_configs
-
-        script_path = self.copy_from_template(f'vivado_power.tcl',
-                                              tb_top=self.postsynthsim_flow.tb_top,
-                                              run_configs=self.run_configs,
-                                              checkpoint=str(
-                                                  self.synth_flow.flow_run_dir / VivadoSynth.checkpoints_dir / 'post_route.dcp')
-                                              )
-
-        return self.run_vivado(script_path, stdout_logfile='vivado_postsynth_power_stdout.log')
-
-    def parse_power_report(self, report_xml):
-        tree = ElementTree.parse(report_xml)
-
-        results = {}
-        components = {}
-
-        for tablerow in tree.findall("./section[@title='Summary']/table/tablerow"):
-            tablecells = tablerow.findall('tablecell')
-            key, value = (html.unescape(
-                x.attrib['contents']).strip() for x in tablecells)
-            results[key] = value
-
-        for tablerow in tree.findall("./section[@title='Summary']/section[@title='On-Chip Components']/table/tablerow"):
-            tablecells = tablerow.findall('tablecell')
-            if len(tablecells) >= 2:
-                contents = [html.unescape(
-                    x.attrib['contents']).strip() for x in tablecells]
-                key = contents[0]
-                value = contents[1]
-                components[key] = value
-        results['Components Power'] = components
-
-        return results
-
-    def parse_reports(self):
-        report_xml = self.flow_run_dir / self.power_report_filename
-
-        results = self.parse_power_report(report_xml)
-
-        self.results['success'] = True
-        self.results.update(**results)
diff --git a/xeda/flows/vivado/vivado_project.py b/xeda/flows/vivado/vivado_project.py
deleted file mode 100644
index fa2db55f..00000000
--- a/xeda/flows/vivado/vivado_project.py
+++ /dev/null
@@ -1,82 +0,0 @@
-# Xeda Vivado Synthtesis flow
-# ©2021 Kamyar Mohajerani and contributors
-
-import logging
-from typing import Union
-from .vivado_synth import VivadoSynth
-from ..flow import SynthFlow
-from .vivado import vivado_generics
-
-logger = logging.getLogger()
-
-class VivadoPrjSynth(VivadoSynth, SynthFlow):
-    default_settings = {**SynthFlow.default_settings, 'nthreads': 4,
-                        'fail_critical_warning': False, 'fail_timing': False,
-                        'optimize_power': False, 'optimize_power_postplace': False}
-
-    required_settings = {'clock_period': Union[str, int]}
-
-    synth_output_dir = 'output'
-    checkpoints_dir = 'checkpoints'
-
-    blacklisted_resources = ['latch']
-
-    def run(self):
-        rtl_settings = self.settings.design["rtl"]
-        flow_settings = self.settings.flow
-        generics_options = vivado_generics(
-            rtl_settings.get("generics", {}), sim=False)
-
-        input_delay = flow_settings.get('input_delay', 0)
-        output_delay = flow_settings.get('output_delay', 0)
-        constrain_io = flow_settings.get('constrain_io', False)
-        # out_of_context = flow_settings.get('out_of_context', False)
-
-        clock_xdc_path = self.copy_from_template(f'clock.xdc',
-                                                 constrain_io=constrain_io,
-                                                 input_delay=input_delay,
-                                                 output_delay=output_delay,
-                                                 )
-
-        self.blacklisted_resources = flow_settings.get(
-            'blacklisted_resources', self.blacklisted_resources)
-        
-
-        logger.info(f"blacklisted_resources: {self.blacklisted_resources}")
-
-        options = dict(
-            synth=dict(),
-            impl=dict(),
-        )
-
-        #TODO find a suitable interface for getting more general args
-
-        for x in ["synth", "impl"]:
-            x_options = flow_settings.get(f"{x}_options")
-            if x_options:
-                if isinstance(x_options, dict):
-                    options[x]=x_options
-                elif isinstance(x_options, str):
-                    options[x]= {k:v for (k,v) in [tuple(kv.split("=")[0:2]) for kv in x_options.split(",")]}
-            # overrides
-            strategy = flow_settings.get(f"{x}_strategy")
-            if strategy:
-                options[x]["strategy"] = strategy
-
-        logger.info(f"options={options}")
-
-        # FIXME
-        # if 'bram_tile' in self.blacklisted_resources:
-        #     # FIXME also add -max_uram 0 for ultrascale+
-        #     options['synth'].append('-max_bram 0')
-        # if 'dsp' in self.blacklisted_resources:
-        #     options['synth'].append('-max_dsp 0')
-
-        script_path = self.copy_from_template(f'vivado_project.tcl',
-                                              xdc_files=[clock_xdc_path],
-                                              options=options,
-                                              generics_options=generics_options,
-                                              synth_output_dir=self.synth_output_dir,
-                                              checkpoints_dir=self.checkpoints_dir
-                                              )
-        return self.run_vivado(script_path)
\ No newline at end of file
diff --git a/xeda/flows/vivado/vivado_sim.py b/xeda/flows/vivado/vivado_sim.py
deleted file mode 100644
index d3bcef6a..00000000
--- a/xeda/flows/vivado/vivado_sim.py
+++ /dev/null
@@ -1,166 +0,0 @@
-# © 2020 [Kamyar Mohajerani](mailto:kamyar@ieee.org)
-
-import logging
-import os
-import math
-from os.path import join
-from types import SimpleNamespace
-from typing import List
-
-from pkg_resources import require
-from ...utils import try_convert, unique
-from ..flow import DesignSource, Flow, SimFlow, DebugLevel
-from ...flows.settings import Settings
-from .vivado_synth import VivadoSynth
-from .vivado import Vivado
-
-logger = logging.getLogger()
-
-
-class VivadoSim(Vivado, SimFlow):
-    """
-    xsim flow
-    Can run multiple configurations (a.k.a testvectors) in a single run of Vivado through "run_configs"
-
-    """
-
-    def run(self):
-        flow_settings = self.settings.flow
-        tb_settings: dict = self.settings.design["tb"]
-        generics = tb_settings.get("generics", {})
-        saif = flow_settings.get('saif')
-
-        elab_flags = flow_settings.get('elab_flags')
-        if not elab_flags:
-            elab_flags = ['-relax']
-
-        elab_flags.append(f'-mt {"off" if self.args.debug else self.nthreads}')
-
-        elab_debug = flow_settings.get('elab_debug')
-        run_configs = flow_settings.get('run_configs')
-        if not elab_debug and (self.args.debug or saif or self.vcd):
-            elab_debug = "typical"
-        if elab_debug:
-            elab_flags.append(f'-debug {elab_debug}')
-
-        if not run_configs:
-            run_configs = [dict(saif=saif, generics=generics,
-                                vcd=self.vcd, name='default')]
-            if self.vcd:
-                logger.info(f"Dumping VCD to {self.flow_run_dir / self.vcd}")
-        else:
-            for idx, rc in enumerate(run_configs):
-                # merge
-                rc['generics'] = {**generics, **rc['generics']}
-                if not 'saif' in rc:
-                    rc['saif'] = saif
-                if not 'name' in rc:
-                    rc['name'] = f'run_{idx}'
-                if not 'vcd' in rc:
-                    rc['vcd'] = (rc['name'] + '_' +
-                                 self.vcd) if self.vcd else None
-
-        tb_uut = tb_settings.get('uut')
-        sdf = flow_settings.get('sdf')
-        if sdf:
-            if not isinstance(sdf, list):
-                sdf = [sdf]
-            for s in sdf:
-                if isinstance(s, str):
-                    s = {"file": s}
-                root = s.get("root", tb_uut)
-                assert root, "neither SDF root nor tb.uut are provided"
-                elab_flags.append(
-                    f'-sdf{s.get("delay", "max")} {root}={s["file"]}')
-
-        libraries = flow_settings.get('libraries')
-        if libraries:
-            elab_flags.extend([f'-L {l}' for l in libraries])
-
-        elab_optimize = flow_settings.get('elab_optimize', '-O3')
-        if elab_optimize and elab_optimize not in elab_flags:  # FIXME none of -Ox in elab_flags
-            elab_flags.append(elab_optimize)
-
-        script_path = self.copy_from_template(f'vivado_sim.tcl',
-                                              analyze_flags=' '.join(flow_settings.get(
-                                                  'analyze_flags', ['-relax'])),
-                                              elab_flags=' '.join(
-                                                  unique(elab_flags)),
-                                              run_configs=run_configs,
-                                              sim_flags=' '.join(
-                                                  flow_settings.get('sim_flags', [])),
-                                              initialize_zeros=False,
-                                              sim_tops=self.sim_tops,
-                                              tb_top=self.tb_top,
-                                              lib_name='work',
-                                              sim_sources=self.sim_sources,
-                                              debug_traces=self.args.debug >= DebugLevel.HIGHEST or self.settings.flow.get(
-                                                  'debug_traces')
-                                              )
-        return self.run_vivado(script_path)
-
-
-class VivadoPostsynthSim(VivadoSim):
-    """depends on VivadoSynth """
-
-    @classmethod
-    def prerequisite_flows(cls, flow_settings, design_settings):
-        synth_overrides = dict(constrain_io=True)
-        period = flow_settings.get('clock_period')
-        if period:
-            synth_overrides['clock_period'] = period
-
-        opt_power = flow_settings.get('optimize_power')
-        if opt_power is not None:
-            synth_overrides['optimize_power'] = opt_power
-
-        return {VivadoSynth: (synth_overrides, {})}
-
-    def __init__(self, settings: Settings, args: SimpleNamespace, completed_dependencies: List[Flow]):
-        self.synth_flow: VivadoSynth = completed_dependencies[0]
-        self.synth_settings = self.synth_flow.settings.flow
-        self.synth_results = self.synth_flow.results
-
-        settings.design['rtl']['sources'] = [DesignSource(self.synth_flow.flow_run_dir / VivadoSynth.synth_output_dir / 'impl_timesim.v')]
-
-        design_settings = settings.design
-        tb_settings = design_settings['tb']
-        flow_settings = settings.flow
-        top = tb_settings['top']
-        if isinstance(top, str):
-            top = [top]
-        if not 'glbl' in top:
-            top.append('glbl')
-        tb_settings['top'] = top
-
-        if 'libraries' not in flow_settings:
-            flow_settings['libraries'] = []
-        if 'simprims_ver' not in flow_settings['libraries']:
-            flow_settings['libraries'].append('simprims_ver')
-
-        netlist_base = os.path.splitext(
-            str(design_settings['rtl']['sources'][0]))[0]
-
-        timing_sim = bool(try_convert(flow_settings.get('timing_sim', True)))
-
-        if timing_sim:
-            if not flow_settings.get('sdf'):
-                flow_settings['sdf'] = {'file': netlist_base + '.sdf'}
-            logger.info(f"Timing simulation using SDF {flow_settings['sdf']}")
-
-        clock_period_ps_generic = tb_settings.get(
-            'clock_period_ps_generic', 'G_PERIOD_PS')  # FIXME
-        tb_settings['generics'] = tb_settings.get(
-            'generics', {})  # optional key, create if not exists
-        if clock_period_ps_generic:
-            clock_ps = math.floor(
-                self.synth_settings['clock_period'] * 1000)
-            tb_settings['generics'][clock_period_ps_generic] = clock_ps
-            for rc in flow_settings.get('run_configs', []):
-                rc['generics'] = rc.get('generics', {})  # create if not exists
-                rc['generics'][clock_period_ps_generic] = clock_ps
-
-        flow_settings['elab_flags'] = ['-relax', '-maxdelay', '-transport_int_delays',
-                                       '-pulse_r 0', '-pulse_int_r 0', '-pulse_e 0', '-pulse_int_e 0']
-
-        VivadoSim.__init__(self, settings, args, completed_dependencies)
diff --git a/xeda/flows/vivado/vivado_synth.py b/xeda/flows/vivado/vivado_synth.py
deleted file mode 100644
index ebddaedd..00000000
--- a/xeda/flows/vivado/vivado_synth.py
+++ /dev/null
@@ -1,383 +0,0 @@
-
-# Xeda Vivado Synthtesis flow
-# ©2021 Kamyar Mohajerani and contributors
-
-from collections import abc
-import copy
-import logging
-from typing import Union
-from ..flow import SynthFlow
-from .vivado import Vivado, vivado_generics
-
-logger = logging.getLogger()
-
-
-class VivadoSynth(Vivado, SynthFlow):
-    default_settings = {**SynthFlow.default_settings, 'nthreads': 4,
-                        'fail_critical_warning': False, 'fail_timing': False,
-                        'optimize_power': False, 'optimize_power_postplace': False}
-
-    required_settings = {'clock_period': Union[str, int]}
-
-    synth_output_dir = 'output'
-    checkpoints_dir = 'checkpoints'
-
-    # see https://www.xilinx.com/support/documentation/sw_manuals/xilinx2020_1/ug904-vivado-implementation.pdf
-    # and https://www.xilinx.com/support/documentation/sw_manuals/xilinx2020_1/ug901-vivado-synthesis.pdf
-    strategy_options = {
-        "Debug": {
-            "synth": ["-assert", "-debug_log",
-                      "-flatten_hierarchy none", "-no_timing_driven", "-keep_equivalent_registers",
-                      "-no_lc", "-fsm_extraction off", "-directive RuntimeOptimized"],
-            "opt": "-directive RuntimeOptimized",
-            "place": "-directive RuntimeOptimized",
-            "place_opt": [],
-            "route": "-directive RuntimeOptimized",
-            "phys_opt": "-directive RuntimeOptimized"
-        },
-
-        "Runtime": {
-            "synth": ["-no_timing_driven", "-directive RuntimeOptimized"],
-            "opt": "-directive RuntimeOptimized",
-            "place": "-directive RuntimeOptimized",
-            "place_opt": [],
-            # with -ultrathreads results are not reproducible!
-            # OR "-no_timing_driven -ultrathreads",
-            "route": ["-directive RuntimeOptimized"],
-            "phys_opt": "-directive RuntimeOptimized"
-        },
-
-        "Default": {
-            "synth": ["-flatten_hierarchy rebuilt", "-directive Default"],
-            "opt": ["-directive ExploreWithRemap"],
-            "place": ["-directive Default"],
-            "place_opt": [],
-            "route": ["-directive Default"],
-            "phys_opt": ["-directive Default"]
-        },
-
-        "Timing": {
-            # -mode: default, out_of_context
-            # -flatten_hierarchy: rebuilt, full; equivalent in terms of QoR?
-            # -no_lc: When checked, this option turns off LUT combining
-            # -keep_equivalent_registers -no_lc
-            "synth": ["-flatten_hierarchy rebuilt",
-                      "-retiming",
-                      "-directive PerformanceOptimized",
-                      "-fsm_extraction one_hot",
-                      #   "-resource_sharing off",
-                      #   "-no_lc",
-                      "-shreg_min_size 5",
-                      #   "-keep_equivalent_registers "
-                      ],
-            "opt": ["-directive ExploreWithRemap"],
-            "place": ["-directive ExtraPostPlacementOpt"],
-            "place_opt": ['-retarget', '-propconst', '-sweep', '-aggressive_remap', '-shift_register_opt'],
-            "phys_opt": ["-directive AggressiveExplore"],
-            "place_opt2": ["-directive Explore"],
-            # "route": "-directive NoTimingRelaxation",
-            "route": ["-directive AggressiveExplore"],
-        },
-        "ExtraTimingCongestion": {
-            "synth": ["-flatten_hierarchy full",
-                      "-retiming",
-                      "-directive PerformanceOptimized",
-                      "-fsm_extraction one_hot",
-                      "-resource_sharing off",
-                      "-shreg_min_size 10",
-                      "-keep_equivalent_registers",
-                      ],
-            "opt": ["-directive ExploreWithRemap"],
-            "place": ["-directive AltSpreadLogic_high"],
-            "place_opt": ['-retarget', '-propconst', '-sweep', '-remap', '-muxf_remap', '-aggressive_remap', '-shift_register_opt'],
-            "place_opt2": ["-directive Explore"],
-            "phys_opt": ["-directive AggressiveExplore"],
-            "route": ["-directive AlternateCLBRouting"],
-        },
-        "ExtraTiming": {
-            "synth": ["-flatten_hierarchy full",
-                      "-retiming",
-                      "-directive PerformanceOptimized",
-                      "-fsm_extraction one_hot",
-                      "-resource_sharing off",
-                      #   "-no_lc",
-                      "-shreg_min_size 10",
-                      "-keep_equivalent_registers",
-                      ],
-            "opt": ["-directive ExploreWithRemap"],
-            "place": "-directive ExtraTimingOpt",
-            "place_opt": ['-retarget', '-propconst', '-sweep', '-muxf_remap', '-aggressive_remap', '-shift_register_opt'],
-            "place_opt2": ["-directive Explore"],
-            "phys_opt": ["-directive AggressiveExplore"],
-            "route": ["-directive NoTimingRelaxation"],
-        },
-        "ExtraTimingAltRouting": {
-            # -mode: default, out_of_context
-            # -flatten_hierarchy: rebuilt, full; equivalent in terms of QoR?
-            # -no_lc: When checked, this option turns off LUT combining
-            # -keep_equivalent_registers -no_lc
-            "synth": ["-flatten_hierarchy full",
-                      "-retiming",
-                      "-directive PerformanceOptimized",
-                      "-fsm_extraction one_hot",
-                      #   "-resource_sharing off",
-                      #   "-no_lc",
-                      "-shreg_min_size 5",
-                      "-keep_equivalent_registers "
-                      ],
-            "opt": ["-directive ExploreWithRemap"],
-            "place": ["-directive ExtraTimingOpt"],
-            "place_opt": ['-retarget', '-propconst', '-sweep', '-aggressive_remap', '-shift_register_opt'],
-            "phys_opt": ["-directive AggressiveExplore"],
-            # "route": "-directive NoTimingRelaxation",
-            "route": ["-directive AlternateCLBRouting"],
-        },
-        "Area": {
-            # AreaOptimized_medium or _high prints error messages in Vivado 2020.1: "unexpected non-zero reference counts", but succeeeds and post-impl sim is OK too
-            "synth": ["-flatten_hierarchy full", "-control_set_opt_threshold 1", "-shreg_min_size 3", "-resource_sharing auto", "-directive AreaOptimized_medium"],
-            # if no directive: -resynth_seq_area
-            "opt": "-directive ExploreArea",
-            "place": "-directive Default",
-            "place_opt": "-directive ExploreArea",
-            # "place_opt": ['-retarget', '-propconst', '-sweep', '-aggressive_remap', '-shift_register_opt',
-            #               '-dsp_register_opt', '-bram_power_opt', '-resynth_seq_area', '-merge_equivalent_drivers'],
-            # if no directive: -placement_opt
-            "phys_opt": "-directive Explore",
-            "route": "-directive Explore",
-        },
-        "AreaHigh": {
-            # AreaOptimized_medium or _high prints error messages in Vivado 2020.1: "unexpected non-zero reference counts", but succeeeds and post-impl sim is OK too
-            "synth": ["-flatten_hierarchy full", "-control_set_opt_threshold 1", "-shreg_min_size 3", "-resource_sharing on", "-directive AreaOptimized_high"],
-            # if no directive: -resynth_seq_area
-            "opt": "-directive ExploreArea",
-            "place": "-directive Default",
-            "place_opt": "-directive ExploreArea",
-            # "place_opt": ['-retarget', '-propconst', '-sweep', '-aggressive_remap', '-shift_register_opt',
-            #               '-dsp_register_opt', '-bram_power_opt', '-resynth_seq_area', '-merge_equivalent_drivers'],
-            # if no directive: -placement_opt
-            "phys_opt": "-directive Explore",
-            "route": "-directive Explore",
-        },
-        "AreaPower": {
-            # AreaOptimized_medium or _high prints error messages in Vivado 2020.1: "unexpected non-zero reference counts", but succeeeds and post-impl sim is OK too
-            "synth": ["-flatten_hierarchy full", "-control_set_opt_threshold 1", "-shreg_min_size 3", "-resource_sharing auto", "-gated_clock_conversion auto", "-directive AreaOptimized_medium"],
-            "opt": ["-directive ExploreArea"],
-            "place": "-directive Default",
-            "place_opt": ['-retarget', '-propconst', '-sweep', '-aggressive_remap', '-shift_register_opt', '-dsp_register_opt', '-resynth_seq_area', '-merge_equivalent_drivers'],
-            "place_opt2": ["-directive ExploreArea"],
-            # FIXME!!! This is the only option that results in correct post-impl timing sim! Why??!
-            "phys_opt": ["-directive AggressiveExplore"],
-            "route": ["-directive Explore"],
-        },
-        "AreaTiming": {
-            "synth": ["-flatten_hierarchy rebuilt", "-retiming"],
-            # if no directive: -resynth_seq_area
-            "opt": ["-directive ExploreWithRemap"],
-            "place": ["-directive ExtraPostPlacementOpt"],
-            # "place_opt": ["-directive ExploreArea"],
-            "place_opt": ['-retarget', '-propconst', '-sweep', '-aggressive_remap', '-shift_register_opt', '-dsp_register_opt', '-resynth_seq_area', '-merge_equivalent_drivers'],
-            "place_opt2": ["-directive ExploreArea"],
-            # if no directive: -placement_opt
-            "phys_opt": "-directive AggressiveExplore",
-            "route": "-directive Explore",
-        },
-        "AreaExploreWithRemap": {
-            "synth": ["-flatten_hierarchy full", "-retiming"],
-            # if no directive: -resynth_seq_area
-            "opt": "-directive ExploreWithRemap",
-            "place": "-directive Default",
-            "place_opt": "-directive ExploreWithRemap",
-            # "place_opt": ['-retarget', '-propconst', '-sweep', '-aggressive_remap', '-shift_register_opt',
-            #               '-dsp_register_opt', '-bram_power_opt', '-resynth_seq_area', '-merge_equivalent_drivers'],
-            # if no directive: -placement_opt
-            "phys_opt": "-directive Explore",
-            "route": "-directive Explore",
-        },
-        "AreaExploreWithRemap2": {
-            "synth": [],
-            # if no directive: -resynth_seq_area
-            "opt": "-directive ExploreArea",
-            "place": "-directive Default",
-            "place_opt": "-directive ExploreWithRemap",
-            # "place_opt": ['-retarget', '-propconst', '-sweep', '-aggressive_remap', '-shift_register_opt',
-            #               '-dsp_register_opt', '-bram_power_opt', '-resynth_seq_area', '-merge_equivalent_drivers'],
-            # if no directive: -placement_opt
-            "phys_opt": "-directive Explore",
-            "route": "-directive Explore",
-        },
-        "AreaExplore": {
-            "synth": ["-flatten_hierarchy full"],
-            # if no directive: -resynth_seq_area
-            "opt": "-directive ExploreArea",
-            "place": "-directive Default",
-            "place_opt": "-directive ExploreArea",
-            # "place_opt": ['-retarget', '-propconst', '-sweep', '-aggressive_remap', '-shift_register_opt',
-            #               '-dsp_register_opt', '-bram_power_opt', '-resynth_seq_area', '-merge_equivalent_drivers'],
-            # if no directive: -placement_opt
-            "phys_opt": "-directive Explore",
-            "route": "-directive Explore",
-        },
-        "Power": {
-            "synth": ["-flatten_hierarchy full", "-gated_clock_conversion auto", "-control_set_opt_threshold 1", "-shreg_min_size 3", "-resource_sharing auto"],
-            # if no directive: -resynth_seq_area
-            "opt": "-directive ExploreSequentialArea",
-            "place": "-directive Default",
-            "place_opt": "-directive ExploreSequentialArea",
-            # ['-retarget', '-propconst', '-sweep', '-aggressive_remap', '-shift_register_opt',
-            #   '-dsp_register_opt', '-bram_power_opt', '-resynth_seq_area', '-merge_equivalent_drivers'],
-            # if no directive: -placement_opt
-            "phys_opt": "-directive Explore",
-            "route": "-directive Explore",
-        }
-    }
-
-    def run(self):
-        rtl_settings = self.settings.design["rtl"]
-        flow_settings = self.settings.flow
-        generics_options = vivado_generics(
-            rtl_settings.get("generics", {}), sim=False)
-
-        input_delay = flow_settings.get('input_delay', 0)
-        output_delay = flow_settings.get('output_delay', 0)
-        constrain_io = flow_settings.get('constrain_io', False)
-        out_of_context = flow_settings.get('out_of_context', False)
-
-        clock_xdc_path = self.copy_from_template(f'clock.xdc',
-                                                 constrain_io=constrain_io,
-                                                 input_delay=input_delay,
-                                                 output_delay=output_delay,
-                                                 )
-
-        strategy = flow_settings.get('strategy', 'Default')
-        if isinstance(strategy, abc.Mapping):
-            options = copy.deepcopy(strategy)
-        else:
-            logger.info(f'Using synthesis strategy: {strategy}')
-            if strategy not in self.strategy_options.keys():
-                self.fatal(f'Unknown strategy: {strategy}')
-            options = copy.deepcopy(self.strategy_options[strategy])
-
-        if 'place_opt2' not in options:
-            options['place_opt2'] = None
-
-        if out_of_context:
-            options['synth'].extend(["-mode", "out_of_context"])
-
-        for k, v in options.items():
-            if isinstance(v, str):
-                options[k] = v.split()
-
-        default_blacklisted_resources = ['latch']
-        # backward compatibility
-        if not self.settings.flow.get('allow_brams', True):
-            default_blacklisted_resources.append('bram_tile')
-
-        if not flow_settings.get('allow_dsps', True):
-            default_blacklisted_resources.append('dsp')
-
-        blacklisted_resources = flow_settings.get(
-            'blacklisted_resources', default_blacklisted_resources)
-        if 'bram' in blacklisted_resources and 'bram_tile' not in blacklisted_resources:
-            blacklisted_resources.append('bram_tile')
-
-        self.blacklisted_resources = blacklisted_resources
-        logger.info(f"blacklisted_resources: {blacklisted_resources}")
-
-        if 'bram_tile' in blacklisted_resources:
-            # FIXME also add -max_uram 0 for ultrascale+
-            options['synth'].append('-max_bram 0')
-        if 'dsp' in blacklisted_resources:
-            options['synth'].append('-max_dsp 0')
-
-        # to strings
-        for k, v in options.items():
-            options[k] = ' '.join(v) if v is not None else None
-
-        script_path = self.copy_from_template(f'{self.name}.tcl',
-                                              xdc_files=[clock_xdc_path],
-                                              options=options,
-                                              generics_options=generics_options,
-                                              synth_output_dir=self.synth_output_dir,
-                                              checkpoints_dir=self.checkpoints_dir
-                                              )
-        return self.run_vivado(script_path)
-
-    def parse_reports(self):
-        report_stage = 'post_route'
-        reports_dir = self.reports_dir / report_stage
-
-        failed = False
-
-        failed |= not self.parse_report_regex(reports_dir / 'timing_summary.rpt',
-                                              r'Design\s+Timing\s+Summary[\s\|\-]+WNS\(ns\)\s+TNS\(ns\)\s+TNS Failing Endpoints\s+TNS Total Endpoints\s+WHS\(ns\)\s+THS\(ns\)\s+THS Failing Endpoints\s+THS Total Endpoints\s+WPWS\(ns\)\s+TPWS\(ns\)\s+TPWS Failing Endpoints\s+TPWS Total Endpoints\s*' +
-                                              r'\s*(?:\-+\s+)+' +
-                                              r'(?P<wns>\-?\d+(?:\.\d+)?)\s+(?P<_tns>\-?\d+(?:\.\d+)?)\s+(?P<_failing_endpoints>\-?\d+(?:\.\d+)?)\s+(?P<_tns_total_endpoints>\-?\d+(?:\.\d+)?)\s+'
-                                              r'(?P<whs>\-?\d+(?:\.\d+)?)\s+(?P<_ths>\-?\d+(?:\.\d+)?)\s+(?P<_ths_failing_endpoints>\-?\d+(?:\.\d+)?)\s+(?P<_ths_total_endpoints>\-?\d+(?:\.\d+)?)\s+',
-                                              r'Clock Summary[\s\|\-]+^\s*Clock\s+.*$[^\w]+(\w*)\s+(\{.*\})\s+(?P<clock_period>\d+(?:\.\d+)?)\s+(?P<clock_frequency>\d+(?:\.\d+)?)'
-                                              )
-
-        # failed |= not self.parse_report(reports_dir / 'power.rpt',
-        #                                 r'^\s*\|\s*Total\s+On-Chip\s+Power\s+\((?P<power_onchip_unit>\w+)\)\s*\|\s*(?P<power_onchip>[\-\.\w]+)\s*\|.*' +
-        #                                 r'^\s*\|\s*Dynamic\s*\((?P<power_dynamic_unit>\w+)\)\s*\|\s*(?P<power_dynamic> [\-\.\w]+)\s*\|.*' +
-        #                                 r'^\s*\|\s*Device\s+Static\s+\((?P<power_static_unit>\w+)\)\s*\|\s*(?P<power_static>[\-\.\w]+)\s*\|.*' +
-        #                                 r'^\s*\|\s*Confidence\s+Level\s*\|\s*(?P<power_confidence_level>[\-\.\w]+)\s*\|.*' +
-        #                                 r'^\s*\|\s*Design\s+Nets\s+Matched\s*\|\s*(?P<power_nets_matched>[\-\.\w]+)\s*\|.*'
-        #                                 )
-
-        report_file = reports_dir / 'utilization.xml'
-        utilization = self.parse_xml_report(report_file)
-        # ordered dict
-        fields = [
-            ('slice', ['Slice Logic Distribution', 'Slice']),
-            ('slice', ['CLB Logic Distribution', 'CLB']),  # Ultrascale+
-            ('lut', ['Slice Logic', 'Slice LUTs']),
-            ('lut', ['Slice Logic', 'Slice LUTs*']),
-            ('lut', ['CLB Logic', 'CLB LUTs']),
-            ('lut', ['CLB Logic', 'CLB LUTs*']),
-            ('lut_logic', ['Slice Logic', 'LUT as Logic']),
-            ('lut_logic', ['CLB Logic', 'LUT as Logic']),
-            ('lut_mem', ['Slice Logic', 'LUT as Memory']),
-            ('lut_mem', ['CLB Logic', 'LUT as Memory']),
-            ('ff', ['Slice Logic', 'Register as Flip Flop']),
-            ('ff', ['CLB Logic', 'CLB Registers']),
-            ('latch', ['Slice Logic', 'Register as Latch']),
-            ('latch', ['CLB Logic', 'Register as Latch']),
-            ('bram_tile', ['Memory', 'Block RAM Tile']),
-            ('bram_tile', ["BLOCKRAM", "Block RAM Tile"]),
-            ('bram_RAMB36', ['Memory', 'RAMB36/FIFO*']),
-            ('bram_RAMB36', ["BLOCKRAM", "RAMB36/FIFO*"]),
-            ('bram_RAMB18', ['Memory', 'RAMB18']),
-            ('bram_RAMB18', ["BLOCKRAM", "RAMB18"]),
-            ('dsp', ['DSP', 'DSPs']),
-            ('dsp', ["ARITHMETIC", "DSPs"]),
-        ]
-        for k, path in fields:
-            if self.results.get(k) is None:
-                path.append("Used")
-                try:
-                    self.results[k] = self.get_from_path(utilization, path)
-                except:
-                    logger.info(
-                        f"{path} not found in the utilization report {report_file}.")
-
-        self.results['_utilization'] = utilization
-
-        if not failed:
-            for res in self.blacklisted_resources:
-                res_util = self.results.get(res)
-                if res_util is not None:
-                    try:
-                        res_util = int(res_util)
-                        if res_util > 0:
-                            logger.critical(
-                                f'{report_stage} utilization report lists {res_util} use(s) of blacklisted resource `{res}`.')
-                            failed = True
-                    except:
-                        logger.warn(
-                            f'Unknown utilization value: `{res_util}` for blacklisted resource `{res}`. Optimistically assuming results are not violating the blacklist criteria.')
-
-            # TODO better fail analysis for vivado
-            failed |= (self.results['wns'] < 0) or (self.results['whs'] < 0) or (
-                self.results['_failing_endpoints'] != 0)
-
-        self.results['success'] = not failed
diff --git a/xeda/flows/yosys/__init__.py b/xeda/flows/yosys/__init__.py
deleted file mode 100644
index f2e579b8..00000000
--- a/xeda/flows/yosys/__init__.py
+++ /dev/null
@@ -1,1 +0,0 @@
-from .yosys import *
diff --git a/xeda/flows/yosys/templates/yosys.ys b/xeda/flows/yosys/templates/yosys.ys
deleted file mode 100644
index cdbd89e6..00000000
--- a/xeda/flows/yosys/templates/yosys.ys
+++ /dev/null
@@ -1,14 +0,0 @@
-{% for src in design.rtl.sources %}
-{%- if src.type == 'verilog' %}
-read_verilog -noautowire {%- if src.variant == 'systemverilog' %} -sv {% endif %} {{src}}
-{%- endif %}
-{%- endfor %}
-
-prep -top {{design.rtl.top}}
-hierarchy -check -simcheck -top {{design.rtl.top}}
-opt
-check {{rtl_check_flags}} -assert
-write_json yosys_rtl.json
-
-synth_{{fpga.family}} {{synth_opts}} -top {{design.rtl.top}}
-write_json netlist.json
\ No newline at end of file
diff --git a/xeda/flows/yosys/yosys.py b/xeda/flows/yosys/yosys.py
deleted file mode 100644
index e45fc08e..00000000
--- a/xeda/flows/yosys/yosys.py
+++ /dev/null
@@ -1,205 +0,0 @@
-
-import logging
-import pkg_resources
-from ..flow import DesignSource, SimFlow, Flow, SynthFlow
-import toml
-
-# from yowasp_yosys import run_yosys
-
-logger = logging.getLogger()
-
-
-class RecursiveNamespace:
-    @classmethod
-    def from_toml(cls, s: str):
-        return RecursiveNamespace(**toml.load(s))
-
-    @staticmethod
-    def map_entry(entry):
-        if isinstance(entry, dict):
-            return RecursiveNamespace(**entry)
-        return entry
-
-    def __init__(self, **kwargs):
-        for key, val in kwargs.items():
-            if type(val) == dict:
-                setattr(self, key, RecursiveNamespace(**val))
-            elif type(val) == list:
-                setattr(self, key, list(map(self.map_entry, val)))
-            else:  # this is the only addition
-                setattr(self, key, val)
-
-
-class FPGA:
-    def __init__(self, part: str, vendor=None) -> None:
-        if part.startswith('LFE'):
-            self.vendor = 'Lattice'
-            part = part.split('-')
-            assert len(part) == 3
-            if part[0].startswith('LFE5U'):
-                if part[0] == 'LFE5UM':
-                    self.family = 'ecp5'  # With SERDES
-                    self.has_serdes = True
-                if part[0] == 'LFE5UM5G':
-                    self.family = 'ecp5-5g'
-                elif part[0] == 'LFE5U':
-                    self.family = 'ecp5'
-                self.capacity = part[1][:-1] + 'k'
-                spg = part[2]
-                self.speed = spg[0]
-                package = spg[1:-1]
-                if package.startswith('BG'):
-                    package = 'CABGA' + package[2:]
-                self.package = package
-                self.grade = spg[-1]
-
-def get_board_data(board):
-    board_toml = pkg_resources.resource_string(
-        'xeda.data.boards.' + board, 'board.toml')
-    assert board_toml
-    board_toml = board_toml.decode('utf-8')
-    return toml.loads(board_toml)
-
-
-
-
-class Yosys(SynthFlow):
-    def run(self):
-        flow_settings = self.settings.flow
-
-        fpga = flow_settings.get('fpga')
-        if not isinstance(fpga, FPGA):
-            fpga = FPGA(fpga)
-
-        synth_opts = [
-            '-abc9',
-            # 'nowidelut'
-            #  '-dff'
-            # '-abc2', '-retime'
-        ]
-
-        rtl_check_flags = [] #'-noinit'
-
-        script_path = self.copy_from_template(
-            f'yosys.ys', fpga=fpga, 
-            rtl_check_flags=" ".join(rtl_check_flags),
-            synth_opts=" ".join(synth_opts))
-        self.run_process(
-            'yosys', ['-q', '-l', 'yosys.log', script_path])
-
-        self.results['success'] = True
-
-
-class NextPnr(SynthFlow):
-    @classmethod
-    def prerequisite_flows(cls, flow_settings, design_settings):
-        board = flow_settings.get('board')
-        board_data = get_board_data(board)
-        print(cls, flow_settings)
-        print(board_data)
-        fpga_part = board_data['fpga']['part']
-        return {Yosys: (dict(fpga=fpga_part, board=board, clock_period=flow_settings.get('clock_period')), {})}
-
-    def run(self):
-        rtl_settings = self.settings.design['rtl']
-        flow_settings = self.settings.flow
-
-        yosys_flow = self.completed_dependencies[0]
-        netlist_json = yosys_flow.flow_run_dir / f'netlist.json'
-
-        assert netlist_json.exists(), "netlist json does not exist"
-
-        board = flow_settings.get('board')
-        fpga = flow_settings.get('fpga')
-        lpf_cfg = flow_settings.get('lpf_cfg')
-
-        if board:
-            fpga = FPGA(get_board_data(board)['fpga']['part'])
-            # TODO from toml
-            lpf_cfg = pkg_resources.resource_filename(
-                'xeda.data.boards.' + board, f'board.lpf')
-            assert lpf_cfg
-
-        if not isinstance(fpga, FPGA):
-            fpga = FPGA(fpga)
-
-        top = rtl_settings['top']
-
-        pnr_tool = f'nextpnr-{fpga.family}'
-
-        freq_mhz = 1000 / flow_settings['clock_period']
-
-        pnr_opts = ['-q', '-l', f'{pnr_tool}.log',
-                    '--json', netlist_json,
-                    '--top', top,
-                    f'--{fpga.capacity}',
-                    '--package', fpga.package,
-                    '--speed', fpga.speed,
-                    '--freq', freq_mhz,
-                    '--sdf', f'{top}.sdf',
-                    #   '--routed-svg', 'routed.svg',
-                    # '--seed'
-                    ]
-        if lpf_cfg:
-            # FIXME check what to do if no board
-            pnr_opts += ['--lpf', lpf_cfg, '--textcfg', f'config.txt']
-
-        self.run_process(pnr_tool, pnr_opts)
-
-        self.results['success'] = True
-
-
-class OpenFpgaLoader(SynthFlow):
-    @classmethod
-    def prerequisite_flows(cls, flow_settings, design_settings):
-        print(cls, flow_settings)
-        board = flow_settings.get('board')
-        assert board, "board not specified!"
-        return {NextPnr: (dict(board=board, clock_period=flow_settings.get('clock_period')), {})}
-
-    def run(self):
-        board = self.settings.flow['board']
-        board_data = get_board_data(board)
-        fpga = FPGA(board_data['fpga']['part'])
-        bitstream = f'{board}.bit'
-        text_cfg = self.completed_dependencies[0].flow_run_dir / 'config.txt'
-        assert text_cfg.exists()
-
-        packer = None
-
-        if fpga.family == 'ecp5': # FIXME from fpga/board
-            packer = 'ecppack'
-
-        if packer:
-            self.run_process(packer, [str(text_cfg), bitstream])
-
-        self.run_process('openFPGALoader',
-                         ['--board', board, '--bitstream', bitstream],
-                         nolog=True
-                         )
-
-        self.results['success'] = True
-
-
-# class PllWrapperGen(SynthFlow):
-#     def run(self):
-#         flow_settings = self.settings.flow
-
-#         board_data = get_board_data(flow_settings.get('board'))
-
-#         freq_mhz = 1000 / flow_settings['clock_period']
-
-#         board_freq = list(board_data['clocks'].values())[0]  # FIXME
-
-#         pll_module = f'__GEN__PLL'
-#         pll_verilog_filename = f'{pll_module}.v'
-
-#         self.run_process('ecppll', ['-n', pll_module, '--clkin_name', 'in_clk', '--clkin', board_freq,
-#                                     '--clkout0_name', 'out_clk', '--clkout0', freq_mhz, '--file', pll_verilog_filename])
-
-#         self.results['generated_design'] = {'rtl': {'top': 'board_top', 'sources': [self.flow_run_dir / pll_verilog_filename] }}
-
-#         self.results['success'] = True
-
-
-
diff --git a/xeda/plugins/lwc/__init__.py b/xeda/plugins/lwc/__init__.py
deleted file mode 100644
index 61e4b87c..00000000
--- a/xeda/plugins/lwc/__init__.py
+++ /dev/null
@@ -1,1 +0,0 @@
-from .flows import *
\ No newline at end of file
diff --git a/xeda/plugins/lwc/flows/__init__.py b/xeda/plugins/lwc/flows/__init__.py
deleted file mode 100644
index 2b8fa856..00000000
--- a/xeda/plugins/lwc/flows/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from .power import VivadoPower as VivadoPowerLwc # alias for old name
-from .power import *
-from .timing_and_verification import *
diff --git a/xeda/plugins/lwc/flows/power.py b/xeda/plugins/lwc/flows/power.py
deleted file mode 100644
index 9fe04780..00000000
--- a/xeda/plugins/lwc/flows/power.py
+++ /dev/null
@@ -1,270 +0,0 @@
-# © 2021 [Kamyar Mohajerani](mailto:kamyar@ieee.org)
-import copy
-import logging
-import os
-import math
-import csv
-from pathlib import Path
-import re
-
-from xeda.flows.flow import DesignSource, SimFlow
-from xeda.flows.vivado.vivado_sim import VivadoPostsynthSim, VivadoSim
-from xeda.flows.vivado.vivado_power import VivadoPower as XedaVivadoPower
-
-from ..lwc import LWC
-
-__all__ = ['VivadoPower', 'VivadoPowerTimingOnly']
-
-
-_logger = logging.getLogger()
-
-_default_power_tvs = ['enc_16_0', 'enc_0_16', 'enc_1536_0',
-                      'enc_0_1536', 'dec_16_0', 'dec_1536_0']
-
-
-class VivadoPower(XedaVivadoPower, LWC):
-    required_settings = {}
-    # default_settings = dict(strategy='ExtraTiming')
-
-    @classmethod
-    def prerequisite_flows(cls, flow_settings, design_settings):
-
-        parent_prereqs = XedaVivadoPower.prerequisite_flows(
-            flow_settings, design_settings)
-
-        postsynthsim_overrides, design_overrides = parent_prereqs[VivadoPostsynthSim]
-
-        postsynthsim_overrides['dependencies'] = postsynthsim_overrides.get('dependencies', {})
-        postsynthsim_overrides['dependencies']['vivado_synth'] = postsynthsim_overrides['dependencies'].get('vivado_synth', {})
-        postsynthsim_overrides['dependencies']['vivado_synth']['strategy'] = 'AreaTiming' ## 'AreaPower'
-
-        postsynthsim_overrides['clock_period'] = flow_settings.get(
-            'clock_period', 13.333)
-        postsynthsim_overrides['optimize_power'] = flow_settings.get(
-            'optimize_power', True)
-        postsynthsim_overrides['prerun_time'] = 100 + \
-            (postsynthsim_overrides['clock_period'] * 4) - 1
-        postsynthsim_overrides['timing_sim'] = True
-        postsynthsim_overrides['stop_time'] = None
-        postsynthsim_overrides['vcd'] = None
-
-        LWC.wrap_design(design_settings)
-
-        tb_settings = design_settings['tb']
-        design_name = design_settings['name']
-        lwc_settings = design_settings.get('lwc', {})
-
-        power_tvs = flow_settings.get('power_tvs')
-        if not power_tvs:
-            power_tvs = _default_power_tvs
-            if LWC.supports_hash(design_settings):
-                power_tvs.extend(['hash_16', 'hash_1536'])
-
-
-        lwc_variant = lwc_settings.get('variant')
-        if not lwc_variant:
-            name_splitted = design_name.split('-')
-            assert len(
-                name_splitted) > 1, "either specify design.lwc.variant or design.name should be ending with -v\d+"
-            lwc_variant = name_splitted[-1]
-            assert re.match(
-                r'v\d+', lwc_variant), "either specify design.lwc.variant or design.name should be ending with -v\d+"
-        power_tvs_root = os.path.join('KAT_GMU', lwc_variant)
-
-        default_generics = tb_settings.get('generics', {})
-
-        def pow_tv_run_config(tv_sub):
-            tv_generics = copy.deepcopy(default_generics)
-            tv_generics['G_MAX_FAILURES'] = 1
-            tv_generics['G_TEST_MODE'] = 0
-            clock_period_ps_generic = tb_settings.get(
-                'clock_period_ps_generic', 'G_PERIOD_PS')
-            clock_ps = math.floor(
-                postsynthsim_overrides['clock_period'] * 1000)
-            tv_generics[clock_period_ps_generic] = clock_ps
-            tv_generics['G_FNAME_LOG'] = f'{tv_sub}_LWCTB_log.txt'
-            for t in ['pdi', 'sdi', 'do']:
-                tv_generics[f'G_FNAME_{t.upper()}'] = DesignSource(
-                    os.path.join(power_tvs_root, tv_sub, f'{t}.txt'))
-            saif = f'{tv_sub}.saif'
-            return dict(generics=tv_generics, saif=str(saif), report=f'{tv_sub}.xml', name=tv_sub)
-
-        postsynthsim_overrides['run_configs'] = [
-            pow_tv_run_config(tv) for tv in power_tvs]
-
-        for t in ['pdi', 'sdi', 'do']:
-            default_generics.pop(f'G_FNAME_{t.upper()}', None)
-
-        tb_settings['generics'] = default_generics
-
-        design_overrides['tb'] = design_overrides.get('tb', {})
-        if 'configuration_specification' not in tb_settings:
-            _logger.info(
-                "setting design.tb.configuration_specification = LWC_TB_wrapper_conf")
-            design_overrides['tb']["configuration_specification"] = "LWC_TB_wrapper_conf"
-
-        return {VivadoPostsynthSim: (postsynthsim_overrides, design_overrides)}
-
-    def parse_reports(self):
-        design_name = self.settings.design['name']
-
-        clock_period = self.synth_settings['clock_period']
-
-        freq = math.floor(1000 / clock_period)
-        fields = {'Design': design_name,
-                  'Frequency (MHz)': freq}
-
-        timing_pat = re.compile(
-            r"PASS \(0\): SIMULATION FINISHED after (?P<cycles>\d+) cycles at (?P<totaltime>.*)")
-
-        statics = {}
-        for rc in self.postsynthsim_settings['run_configs']:
-            name = rc['name']
-            report_xml = self.flow_run_dir / rc['report']
-
-            results = self.parse_power_report(report_xml)
-
-            assert results['Design Nets Matched'].startswith('100%')
-            assert results['Confidence Level'] == 'High'
-            
-            def get_power(s):
-                res = results.get(f'{s} (W)')
-                if res is None:
-                    res = round(float(results[f'{s} (mW)']) / 1000, 6)
-                return res
-
-            statics[f'Static:{name}'] = get_power('Device Static')
-
-            fields[name] = get_power('Dynamic')
-
-            lwctb_log = self.postsynthsim_flow.flow_run_dir / \
-                f'{name}_LWCTB_log.txt'
-            with open(lwctb_log) as f:
-                match = timing_pat.search(f.read())
-                assert match, f"timing pattern not found in the LWC_TB log {lwctb_log}. Make sure simulation has not failed and that you are using the correct version of LWC_TB"
-                results['cycles'] = match.group('cycles')
-                fields[name + '_cycles'] = results['cycles']
-                results['totaltime'] = match.group('totaltime')
-            self.results[name] = results
-
-        for x in ['hash_16', 'hash_1536']:
-            if x not in fields:
-                y = x + '_cycles'
-                fields[x] = fields.get(x)
-                # being over-paranoid not to loose anything
-                fields[y] = fields.get(y)
-
-        fields['LUT'] = self.synth_results['lut']
-        fields['FF'] = self.synth_results['ff']
-        fields['Slice'] = self.synth_results['slice']
-        fields['LUT RAM'] = self.synth_results['lut_mem']
-        fields['Static'] = list(statics.values())[0]
-        fields.update(statics)
-
-        # self.run_path.parent ?
-        csv_path = self.results_dir / f"VivadoPowerLwc_{design_name}_{freq}MHz.csv"
-        with open(csv_path, "w") as csv_file:
-            writer = csv.DictWriter(csv_file, fields.keys())
-            writer.writeheader()
-            writer.writerow(fields)
-
-        _logger.info(f"CSV power results written to {csv_path}")
-        self.results['success'] = True
-
-
-class VivadoPowerTimingOnly(SimFlow, LWC):
-    """
-    just trying to reuse VivadoPowerLWc code to redo quick cycle accurate simulation and retrieve missing data
-    """
-    @classmethod
-    def prerequisite_flows(cls, flow_settings, design_settings):
-
-        flow_overrides = {}
-        design_overrides = {}
-
-        flow_overrides['clock_period'] = flow_settings.get(
-            'clock_period', 13.333)
-        flow_overrides['timing_sim'] = False
-        flow_overrides['stop_time'] = None
-        flow_overrides['saif'] = None
-        flow_overrides['vcd'] = None
-
-        tb_settings = design_settings['tb']
-
-        power_tvs = flow_settings.get('power_tvs')
-        if not power_tvs:
-            power_tvs = _default_power_tvs
-            if LWC.supports_hash(design_settings):
-                power_tvs.extend(['hash_16', 'hash_1536'])
-
-        power_tvs_root = os.path.join('KAT_GMU', LWC.variant(design_settings))
-
-        default_generics = tb_settings.get('generics', {})
-
-        def pow_tv_run_config(tv_sub):
-            tv_generics = copy.deepcopy(default_generics)
-            tv_generics['G_MAX_FAILURES'] = 1
-            tv_generics['G_TEST_MODE'] = 0
-            tv_generics['G_FNAME_LOG'] = f'{tv_sub}_LWCTB_log.txt'
-            clock_period_ps_generic = tb_settings.get(
-                'clock_period_ps_generic', 'G_PERIOD_PS')
-            clock_ps = math.floor(
-                flow_overrides['clock_period'] * 1000)
-            tv_generics[clock_period_ps_generic] = clock_ps
-            for t in ['pdi', 'sdi', 'do']:
-                tv_generics[f'G_FNAME_{t.upper()}'] = DesignSource(
-                    os.path.join(power_tvs_root, tv_sub, f'{t}.txt'))
-            return dict(generics=tv_generics, saif=None, report=f'{tv_sub}.xml', name=tv_sub)
-
-        flow_overrides['run_configs'] = [
-            pow_tv_run_config(tv) for tv in power_tvs]
-        for t in ['pdi', 'sdi', 'do']:
-            default_generics.pop(f'G_FNAME_{t.upper()}', None)
-        tb_settings['generics'] = default_generics
-
-        design_overrides['tb'] = design_overrides.get('tb', {})
-        if 'configuration_specification' not in tb_settings:
-            design_overrides['tb']["configuration_specification"] = "LWC_TB_wrapper_conf"
-
-        return {VivadoSim: (flow_overrides, design_overrides)}
-
-    def run(self):
-        _logger.info("running nothing!")
-
-    def parse_reports(self):
-        design_name = self.settings.design['name']
-
-        sim_flow = self.completed_dependencies[0]
-        sim_flow_settings = sim_flow.settings.flow
-
-        clock_period = sim_flow_settings['clock_period']
-
-        freq = math.floor(1000 / clock_period)
-        fields = {'Design': design_name,
-                  'Frequency (MHz)': freq, 'Static': None}
-
-        timing_pat = re.compile(
-            r"PASS \(0\): SIMULATION FINISHED after (?P<cycles>\d+) cycles at (?P<totaltime>.*)")
-
-        for rc in sim_flow_settings['run_configs']:
-            name = rc['name']
-            results = {}
-
-            lwctb_log = sim_flow.flow_run_dir / f'{name}_LWCTB_log.txt'
-            with open(lwctb_log) as f:
-                match = timing_pat.search(f.read())
-                assert match, f"timing pattern not found in the LWC_TB log {lwctb_log}. Make sure simulation has not failed and you are using the correct version of LWC_TB"
-                results['cycles'] = match.group('cycles')
-                fields[name + '_cycles'] = results['cycles']
-                results['totaltime'] = match.group('totaltime')
-            self.results[name] = results
-
-        csv_path = self.results_dir / \
-            f"VivadoPowerLwcTimingOnly_{design_name}_{freq}MHz.csv"
-        with open(csv_path, "w") as csv_file:
-            writer = csv.DictWriter(csv_file, fields.keys())
-            writer.writeheader()
-            writer.writerow(fields)
-
-        _logger.info(f"CSV results written to {csv_path}")
-        self.results['success'] = True
diff --git a/xeda/plugins/lwc/flows/timing_and_verification.py b/xeda/plugins/lwc/flows/timing_and_verification.py
deleted file mode 100644
index 4b98b8df..00000000
--- a/xeda/plugins/lwc/flows/timing_and_verification.py
+++ /dev/null
@@ -1,361 +0,0 @@
-from copy import deepcopy
-import logging
-import os
-import re
-from types import SimpleNamespace
-from typing import List
-from shutil import copyfile
-from xeda.flows.flow import FileResource, Flow, removesuffix
-from xeda.flows.settings import Settings
-from xeda.flows.vivado.vivado_sim import VivadoSim
-import json
-from ..lwc import LWC
-
-__all__ = ['VivadoSimTiming', 'VivadoSimVerification']
-
-_logger = logging.getLogger()
-
-
-class VivadoSimTiming(VivadoSim, LWC):
-    def __init__(self, settings: Settings, args: SimpleNamespace, completed_dependencies: List['Flow']):
-        tb_settings = settings.design.get('tb', {})
-        lwc_settings = settings.design.get('lwc', {})
-
-        tb_settings['top'] = 'LWC_TB'
-
-        if not lwc_settings.get('two_pass'):
-            tb_settings['configuration_specification'] = None
-        elif tb_settings.get('configuration_specification'):
-            _logger.warning(
-                f"Two-pass LWC with design.tb.configuration_specification={tb_settings['configuration_specification']} -> LWC_TB_2pass_conf")
-            tb_settings['configuration_specification'] = 'LWC_TB_2pass_conf'
-
-        tb_generics = tb_settings.get('generics', {})
-        tb_generics['G_MAX_FAILURES'] = 1
-        tb_generics['G_TEST_MODE'] = 4
-
-        tb_settings['generics'] = tb_generics  # in case they didn't exist
-        settings.design['tb'] = tb_settings
-
-        tv_root = 'KAT'
-        variant = LWC.variant(settings.design)
-
-        for t in ['pdi', 'sdi', 'do']:
-            gen = f'G_FNAME_{t.upper()}'
-            if gen in tb_generics:
-                del tb_generics[gen]
-
-        tvs = ['generic_aead_sizes_new_key']
-
-        supports_hash = LWC.supports_hash(settings.design)
-
-        block_size_bits = lwc_settings.get('block_bits')
-        if not block_size_bits or 'AD' not in block_size_bits or 'PT' not in block_size_bits or (supports_hash and 'HM' not in block_size_bits):
-            _logger.critical(
-                f'Missing required design.lwc.block_bits settings for {settings.design.get("name")}. design.lwc.block_bits is {block_size_bits}')
-            exit(1)
-
-        if lwc_settings.get('key_reuse', lwc_settings.get('reuse_key')):
-            tvs.extend([('generic_aead_sizes_reuse_key', 'Reuse Key')])
-        if supports_hash:
-            tvs.extend(['basic_hash_sizes'])
-
-        run_configs = []
-
-        for tv_subfolder in tvs:
-            suffix = None
-            if isinstance(tv_subfolder, tuple):
-                tv_subfolder, suffix = tv_subfolder
-            rc_generics = deepcopy(tb_generics)
-            rc_generics['G_FNAME_TIMING'] = f'LWC_TB_timing_{tv_subfolder}.log'
-            rc_generics['G_FNAME_LOG'] = f'LWC_TB_log_{tv_subfolder}.log'
-
-            for t in ['pdi', 'sdi', 'do']:
-                rc_generics[f'G_FNAME_{t.upper()}'] = FileResource(
-                    os.path.join(tv_root, variant, tv_subfolder, f'{t}.txt'))
-
-            run_configs.append(
-                dict(generics=rc_generics, name=tv_subfolder, suffix=suffix))
-
-        settings.flow['run_configs'] = run_configs
-
-        super().__init__(settings, args, completed_dependencies)
-
-    def parse_reports(self):
-        VivadoSim.parse_reports(self)
-        results = self.results
-
-        if not results.get('success'):
-            results['success'] = False
-            return
-
-        tb_settings = self.settings.design['tb']
-
-        run_configs = self.settings.flow.get('run_configs', [dict(
-            generics=tb_settings.get('generics', {}), name="DEFAULT")])
-
-        success_pat = re.compile(
-            r"PASS \(0\): SIMULATION FINISHED after (?P<cycles>\d+) cycles at (?P<totaltime>.*)")
-        pdi_pat = re.compile(
-            r"####\s+(?P<op>\w+.*)\s*\n\s*#### MsgID\s*=\s*(?P<msgid>\d+)\s*,?\s*KeyID\s*=\s*(?P<keyid>\d+)\s*,?\s*(?P<the_rest>\w+.*)\s*\n", re.MULTILINE | re.IGNORECASE)
-
-        timing_results = {}
-        for rc in run_configs:
-            rc_results = {}
-            rc_generics = rc['generics']
-            rc_name = rc['name']
-            suffix = rc.get('suffix')
-
-            lwctb_log = self.flow_run_dir / rc_generics['G_FNAME_LOG']
-            with open(lwctb_log) as f:
-                match = success_pat.search(f.read())
-                if not match:
-                    _logger.critical(
-                        f"timing pattern not found in the LWC_TB log {lwctb_log}. Make sure simulation has not failed and that you are using the correct version of LWC_TB")
-                    self.results['success'] = False
-                    return
-            rc_results['postreset_cycles'] = match.group('cycles')
-            rc_results['total_sim_time'] = match.group('totaltime')
-
-            timing_txt = self.flow_run_dir / rc_generics['G_FNAME_TIMING']
-            with open(timing_txt) as f:
-                timings = f.readlines()
-                if not timings:
-                    _logger.critical(f"Timings file {timing_txt} is empty!")
-                    self.results['success'] = False
-                    return
-
-            pdi: FileResource = rc_generics[f'G_FNAME_PDI']
-            sdi = rc_generics.get(f'G_FNAME_SDI')
-            do = rc_generics[f'G_FNAME_DO']
-
-            with open(pdi.file) as f:
-                content = f.read()
-
-            def short_op(opstr):
-                if opstr == 'Authenticated Encryption':
-                    return 'Encrypt'
-                if opstr == 'Authenticated Decryption':
-                    return 'Decrypt'
-                return opstr
-
-            msgs = []
-            for match in pdi_pat.finditer(content):
-                op = short_op(match.group('op'))
-                d = dict(op=op, msgid=match.group('msgid'),
-                         keyid=match.group('keyid'))
-                the_rest = match.group('the_rest')
-                the_rest = re.split(r'\s*,\s*', the_rest)
-                for x in the_rest:
-                    x = re.split(r'\s*=\s*', x.strip())
-                    d[x[0]] = int(x[1])
-                msgs.append(d)
-
-            rc_results['PDI'] = str(pdi)
-            rc_results['SDI'] = str(sdi)
-            rc_results['DO'] = str(do)
-
-            tp = []
-            for t in timings:
-                t = t.strip()
-                p = re.split(f'\s*,\s*', t)
-                assert len(p) == 2, "should be pairs of msgid, cycles"
-                tp.append(tuple(p))
-
-            assert len(msgs) == len(
-                tp), f"count of messages in {pdi} and timings from {timing_txt} do not match!"
-
-            for msg, (id, cycles) in zip(msgs, tp):
-                assert msg['msgid'] == id
-                op = msg['op']
-                if suffix:
-                    op += ' (' + suffix + ')'
-                msg = {removesuffix(k, 'Size').strip().upper(
-                ): v for k, v in msg.items() if k not in ['keyid', 'op', 'msgid']}
-
-                timing_results[op] = timing_results.get(
-                    op, []) + [(msg, cycles)]
-
-            results['TV:' + rc_name] = rc_results
-
-        supports_hash = LWC.supports_hash(self.settings.design)
-
-        lwc_settings = self.settings.design.get('lwc', {})
-        block_size_bits = lwc_settings.get('block_bits')
-
-        if supports_hash:
-            assert 'HM' in block_size_bits
-
-        block_size_bits['CT'] = block_size_bits['PT']
-        sizes = [16, 64, 1536]
-
-        csv_lines = []
-
-
-        # enc, dec
-        for op , tr in timing_results.items():
-            bsx4 = None
-            bsx5 = None
-            row = []
-            if not tr:
-                continue
-            if op != 'Hash':
-                xt = 'PT' if op.startswith('Encrypt') else 'CT'
-                # Cryptic code ahead
-                for msg_type in ['AD', xt, None]:
-                    msg_type = (msg_type,) if msg_type else ('AD', xt)
-                    bsizes = [block_size_bits[m] for m in msg_type]
-                    xxz = [tuple(sz for _ in bsizes) for sz in sizes] + \
-                        [tuple(bs * j // 8 for bs in bsizes) for j in range(4, 6)]
-                    for idx, sz in enumerate(xxz):
-                        for msg, cycle in tr:
-                            right_value = all(z == msg.get(m)
-                                              for m, z in zip(msg_type, sz))
-                            others_zero = all(
-                                v == 0 for k, v in msg.items() if k not in msg_type)
-                            if right_value and others_zero:
-                                if idx == 3:
-                                    bsx4 = cycle
-                                elif idx == 4:
-                                    bsx5 = cycle
-                                else:
-                                    row.append((f'{"+".join(msg_type)}_{"+".join(map(str,sz))}', cycle))
-                                break
-
-                    if bsx4 and bsx5:
-                        row.append((f'{"+".join(msg_type)}_Long', str(int(bsx5) - int(bsx4))))
-
-            else:
-                hm_sizes = sizes + [block_size_bits['HM']
-                                    * j // 8 for j in range(4, 6)]
-                for idx, sz in enumerate(hm_sizes):
-                    for msg, cycle in timing_results.get(op, []):
-                        if msg.get('HM') == sz:
-                            if idx == 3:
-                                bsx4 = cycle
-                            elif idx == 4:
-                                bsx5 = cycle
-                            else:
-                                row.append((f'HM_{sz}', cycle))
-                            break
-                if bsx4 and bsx5:
-                    row.append((f'HM_Long', str(int(bsx5) - int(bsx4))))
-
-            csv_lines.append(op)
-            ks, vs = tuple(zip(*row))
-            csv_lines.append(', '.join(ks))
-            csv_lines.append(', '.join(vs))
-            csv_lines.append("")
-
-        name = self.settings.design.get('name', "<NO-NAME>")
-        csv_files = self.results_dir / (name + '_Timing.csv')
-        with open(csv_files, 'w') as f:
-            f.writelines([l + '\n' for l in csv_lines])
-
-        _logger.info(f"Timing results written to {csv_files}")
-
-        results.update(timing_results)
-
-        results['success'] = True
-
-
-class VivadoSimVerification(VivadoSim, LWC):
-    def __init__(self, settings: Settings, args: SimpleNamespace, completed_dependencies: List['Flow']):
-        tb_settings = settings.design.get('tb', {})
-
-        lwc_settings = settings.design.get('lwc', {})
-
-        tb_settings['top'] = 'LWC_TB'
-        
-        if not lwc_settings.get('two_pass'):
-            tb_settings['configuration_specification'] = None
-        elif tb_settings.get('configuration_specification'):
-            _logger.warning(
-                f"Two-pass LWC with design.tb.configuration_specification={tb_settings['configuration_specification']} -> LWC_TB_2pass_conf")
-            tb_settings['configuration_specification'] = 'LWC_TB_2pass_conf'
-
-        tb_generics = tb_settings.get('generics', {})
-        tb_generics['G_MAX_FAILURES'] = 100
-        tb_generics['G_TEST_MODE'] = 0  # TODO
-
-        tb_settings['generics'] = tb_generics  # in case they didn't exist
-        settings.design['tb'] = tb_settings
-
-        tv_root = 'KAT'
-        variant = LWC.variant(settings.design)
-
-        for t in ['pdi', 'sdi', 'do']:
-            gen = f'G_FNAME_{t.upper()}'
-            if gen in tb_generics:
-                del tb_generics[gen]
-
-        tvs = ['kats_for_verification']
-
-        if LWC.supports_hash(settings.design):
-            tvs.extend(['blanket_hash_test'])
-
-        run_configs = []
-
-        for tv_subfolder in tvs:
-            rc_generics = deepcopy(tb_generics)
-            rc_generics['G_FNAME_LOG'] = f'LWC_TB_log_{tv_subfolder}.log'
-            rc_generics['G_FNAME_FAILED_TVS'] = f'{tv_subfolder}_failed_testvectors.txt'
-            for t in ['pdi', 'sdi', 'do']:
-                rc_generics[f'G_FNAME_{t.upper()}'] = FileResource(
-                    os.path.join(tv_root, variant, tv_subfolder, f'{t}.txt'))
-
-            run_configs.append(dict(generics=rc_generics, name=tv_subfolder))
-
-        settings.flow['run_configs'] = run_configs
-
-        super().__init__(settings, args, completed_dependencies)
-
-    def parse_reports(self):
-        VivadoSim.parse_reports(self)
-        results = self.results
-
-        if not results.get('success'):
-            results['success'] = False
-            return
-
-        tb_settings = self.settings.design['tb']
-
-        run_configs = self.settings.flow.get('run_configs', [dict(
-            generics=tb_settings.get('generics', {}), name="DEFAULT")])
-
-        success_pat = re.compile(
-            r"PASS \(0\): SIMULATION FINISHED after (?P<cycles>\d+) cycles at (?P<totaltime>.*)")
-        name = self.settings.design.get('name', "<NO-NAME>")
-        results['success'] = True
-        for rc in run_configs:
-            rc_results = {}
-            rc_generics = rc['generics']
-            rc_name = rc['name']
-
-            lwctb_log = self.flow_run_dir / rc_generics['G_FNAME_LOG']
-            with open(lwctb_log) as f:
-                match = success_pat.search(f.read())
-                if not match:
-                    _logger.critical(
-                        f"timing pattern not found in the LWC_TB log {lwctb_log}. Make sure simulation has not failed and that you are using the correct version of LWC_TB")
-                    self.results['success'] = False
-                    copyfile(self.flow_run_dir / rc_generics['G_FNAME_FAILED_TVS'], self.results_dir / (name+'_'+rc_generics['G_FNAME_FAILED_TVS']))
-                else:    
-                    rc_results['postreset_cycles'] = match.group('cycles')
-                    rc_results['total_sim_time'] = match.group('totaltime')
-
-            pdi: FileResource = rc_generics[f'G_FNAME_PDI']
-            sdi = rc_generics.get(f'G_FNAME_SDI')
-            do = rc_generics[f'G_FNAME_DO']
-
-            rc_results['PDI'] = str(pdi)
-            rc_results['SDI'] = str(sdi)
-            rc_results['DO'] = str(do)
-
-            results['TV:' + rc_name] = rc_results
-
-
-        success_file = self.results_dir / (name + '_verification_results.json')
-        
-        with open(success_file, 'w') as f:
-            json.dump(results, f,indent=4)
\ No newline at end of file
diff --git a/xeda/plugins/lwc/lwc.py b/xeda/plugins/lwc/lwc.py
deleted file mode 100644
index 79d7f93f..00000000
--- a/xeda/plugins/lwc/lwc.py
+++ /dev/null
@@ -1,44 +0,0 @@
-import logging
-import re
-from xeda.flows.flow import Flow
-
-_logger = logging.getLogger()
-
-class LWC:
-    @classmethod
-    def variant(cls, design_settings):
-        design_name = design_settings['name']
-        lwc_settings = design_settings.get('lwc', {})
-        lwc_variant = lwc_settings.get('variant')
-        if not lwc_variant:
-            name_splitted = design_name.split('-')
-            assert len(
-                name_splitted) > 1, "either specify design.lwc.variant or design.name should be ending with -v\d+"
-            lwc_variant = name_splitted[-1]
-            assert re.match(
-                r'v\d+', lwc_variant), "either specify design.lwc.variant or design.name should be ending with -v\d+"
-
-        return lwc_variant
-
-    @classmethod
-    def supports_hash(cls, design_settings):
-        lwc_settings = design_settings.get('lwc', {})
-        algorithms = lwc_settings.get('algorithm')
-        if lwc_settings.get('supports_hash'):
-            return True
-        return (algorithms and (isinstance(algorithms, list) or isinstance(algorithms, tuple)) and len(algorithms) > 1)
-
-    @classmethod
-    def wrap_design(cls, design_settings):
-        lwc = design_settings.get('lwc', {})
-        lwc_wrapper = lwc.get('wrapper')
-        if lwc_wrapper:
-            two_pass = lwc.get('two_pass')
-            for section in 'rtl', 'tb':
-                for k,v in lwc_wrapper.get(section, {}).items():
-                    if k == 'sources':
-                        _logger.info(f"Extending design.{section}.{k} with sources from design.lwc.wrapper.{section}.{k}")
-                        design_settings[section][k] += [x for x in v if x not in design_settings[section].get(k, {})]
-                    else:
-                        _logger.info(f"Replacing design.{section}.{k} with design.lwc.wrapper.{section}.{k}={v}")
-                        design_settings[section][k] = v
diff --git a/xeda/utils.py b/xeda/utils.py
deleted file mode 100644
index 93142d7f..00000000
--- a/xeda/utils.py
+++ /dev/null
@@ -1,87 +0,0 @@
-import re
-import csv
-import importlib
-from typing import Any, List
-
-
-def unique(lst: List[Any]) -> List[Any]:
-    """uniquify list while preserving order"""
-    # compatible with Python 3.6 ???
-    # seen = set()
-    # return [x for x in lst if x not in seen and not seen.add(x)]
-    return list(dict.fromkeys(lst))
-
-def camelcase_to_snakecase(name: str) -> str:
-    name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
-    return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower()
-
-
-def snakecase_to_camelcase(name: str) -> str:
-    return ''.join(word.title() for word in name.split('_'))
-
-
-def load_class(full_class_string: str, defualt_module_name=None) -> type:
-    cls_path_lst = full_class_string.split(".")
-    assert len(cls_path_lst) > 0
-
-    cls_name = snakecase_to_camelcase(cls_path_lst[-1])
-    if len(cls_path_lst)  == 1: # module name not specified, use default
-        mod_name = defualt_module_name
-    else:
-        mod_name = ".".join(cls_path_lst[:-1])
-    assert mod_name
-    
-    module = importlib.import_module(mod_name, __package__ if mod_name.startswith('.') else None )
-    return getattr(module, cls_name)
-
-
-def dict_merge(base_dct, merge_dct, add_keys=True):
-    rtn_dct = base_dct.copy()
-    if add_keys is False:
-        merge_dct = {key: merge_dct[key] for key in set(rtn_dct).intersection(set(merge_dct))}
-
-    rtn_dct.update({
-        key: dict_merge(rtn_dct[key], merge_dct[key], add_keys=add_keys)
-        if isinstance(rtn_dct.get(key), dict) and isinstance(merge_dct[key], dict)
-        else merge_dct[key]
-        for key in merge_dct.keys()
-    })
-    return rtn_dct
-
-
-def try_convert(s, convert_lists=False, to_str=True):
-    if s is None:
-        return 'None'
-    if isinstance(s, str): # always?
-        if s.startswith('"') or s.startswith('\''):
-            return s.strip('"\'')
-        if convert_lists and s.startswith('[') and s.endswith(']'):
-            s = re.sub(r'\s+', '', s)
-            return [try_convert(e) for e in s.strip('][').split(',')]
-        # Should NOT convert dict, set, etc!
-
-    try:
-        return int(s)
-    except Exception:
-        try:
-            return float(s)
-        except Exception:
-            s1 = str(s)
-            if s1.lower in ['true', 'yes']:
-                return True
-            if s1.lower in ['false', 'no']:
-                return False
-            return s1 if to_str else s
-
-
-def parse_csv(path, id_field, field_parser=(lambda x: x), id_parser=(lambda x: x), interesting_fields=None):
-    data = {}
-
-    with open(path, newline='') as csvfile:
-        reader = csv.DictReader(csvfile)
-        for row in reader:
-            if not interesting_fields:
-                interesting_fields = row.keys()
-            id = id_parser(row[id_field])
-            data[id] = {k: field_parser(row[k]) for k in interesting_fields}
-        return data
diff --git a/xeda/xeda_app.py b/xeda/xeda_app.py
deleted file mode 100644
index 349d52b3..00000000
--- a/xeda/xeda_app.py
+++ /dev/null
@@ -1,299 +0,0 @@
-# © 2020 [Kamyar Mohajerani](mailto:kamyar@ieee.org)
-
-from datetime import datetime
-import inspect
-import multiprocessing
-import os
-from pathlib import Path
-import sys
-import argparse
-from .flows.flow import Flow, SimFlow, SynthFlow
-from .utils import camelcase_to_snakecase, load_class
-import coloredlogs
-import logging
-import pkg_resources
-
-from .debug import DebugLevel
-from .flow_runner import DefaultRunner, FlowRunner
-import toml
-import json
-import shtab
-
-logger = logging.getLogger()
-logger.setLevel(logging.INFO)
-
-
-try:
-    __version__ = pkg_resources.get_distribution(__package__).version
-except pkg_resources.DistributionNotFound:
-    __version__ = '(N/A - Local package)'
-
-
-class ListDesignsAction(argparse.Action):
-    def __call__(self, parser, namespace, values, option_string=None):
-        try:
-            xp = load_xedaproject(namespace.xedaproject)
-            print(f'Listing designs in `{namespace.xedaproject}`:')
-            designs = xp.get('design')
-            if designs:
-                if not isinstance(designs, list):
-                    designs = [designs]
-                for d in designs:
-                    dn = d.get('name')
-                    if not dn:
-                        dn = '!!!<UNKNOWN>!!!'
-                    desc = d.get('description')
-                    if desc:
-                        desc = ": " + desc
-                    else:
-                        desc = ""
-                    print(f"{' '*4}{dn:<10} {desc:.80}")
-        finally:
-            exit(0)
-
-
-def get_main_argparser():
-
-    # TODO registered plugins
-    flow_classes = inspect.getmembers(sys.modules['xeda.flows'],
-                                      lambda cls: inspect.isclass(cls) and issubclass(cls, Flow) and cls != Flow and cls != SimFlow and cls != SynthFlow)
-    runner_classes = inspect.getmembers(sys.modules['xeda.flow_runner'],
-                                        lambda cls: inspect.isclass(cls) and issubclass(cls, FlowRunner) and cls != FlowRunner)
-
-    parser = argparse.ArgumentParser(
-        prog=__package__,
-        description=f'{__package__}: Cross-EDA abstraction and automation. Version: {__version__}',
-        formatter_class=lambda prog: argparse.HelpFormatter(
-            prog, max_help_position=35),
-    )
-
-    parser.add_argument(
-        '--debug',
-        type=int,
-        metavar='DEBUG_LEVEL',
-        default=DebugLevel.NONE,
-        help=f'Set debug level. Values of DEBUG_LEVEL correspond to: {list(DebugLevel)}'
-    )
-    parser.add_argument(
-        '--verbose',
-        action='store_true',
-        help='Be verbose. Print everything to stdout.'
-    )
-    parser.add_argument(
-        '--quiet',
-        action='store_true',
-        help="Be as quiet as possible. Never print out output from command executions"
-    )
-    parser.add_argument(
-        '--force-run-dir',
-        help='USE ONLY FOR DEBUG PURPOSES.',
-        # default=None
-    )
-    parser.add_argument(
-        '--xeda-run-dir',
-        help='Directory where the flows are executed and intermediate and result files reside.',
-        default=None
-    )
-    parser.add_argument(
-        '--force-rerun',
-        action='store_true',
-        default=False,
-        help='Force re-run of flow and all dependencies, even if they are already up-to-date',
-    )
-    parser.add_argument(
-        '--use-stale',
-        action='store_true',
-        default=False,
-        help="Don'r run the flow if stale results already exist.",
-    )
-    parser.add_argument(
-        '--max-cpus',
-        default=max(1, multiprocessing.cpu_count()), type=int,
-    )
-
-    registered_flows = [camelcase_to_snakecase(n) for n, _ in flow_classes]
-
-    class CommandAction(argparse.Action):
-        def __call__(self, parser, args, value, option_string=None):
-            assert value, "flow should not be empty"
-            # TODO FIXME should change to be class and remove load_class from runners
-            splitted = value.split(':')
-            flow_name = splitted[-1]
-            if len(splitted) == 2:
-                flow_runner_name = splitted[0]
-                if not flow_runner_name.endswith('_runner'):
-                    flow_runner_name += '_runner'
-                try:
-                    # FIXME: search plugins too
-                    args.flow_runner = load_class(
-                        flow_runner_name, '.flow_runner')
-                except:
-                    sys.exit(f'FlowRunner {flow_runner_name} not found')
-            elif len(splitted) == 1:
-                args.flow_runner = DefaultRunner
-            else:
-                sys.exit(f'Use [RunnerName]:flow_name')
-            # if not flow_name in registered_flows:  # FIXME check when loading
-            #     sys.exit(f'Flow {flow_name} not found')
-            setattr(args, self.dest, flow_name)
-
-    parser.add_argument('flow', metavar='[RUNNER_NAME:]FLOW_NAME', action=CommandAction,
-                        help=(f'Flow name optionally prepended by flow-runner.'
-                              'If runner is not specified the default runner is used.\n'
-                              f'Available flows are: {registered_flows}\n'
-                              f'Available runners are: {[camelcase_to_snakecase(n) for n, _ in runner_classes]}'
-                              )
-                        )
-    # redundant, kept for compatibility
-    parser.add_argument(
-        '--design',
-        nargs='?',
-        help='Specify design.name in case multiple designs are available in the Xeda project.'
-    )
-    parser.add_argument(
-        'design',
-        nargs='?',
-        help='Specify design.name in case multiple designs are available in the Xeda project.'
-    )
-    parser.add_argument(
-        '--list-designs',
-        nargs=0,
-        action=ListDesignsAction,
-        help='List all designs available in the Xeda project.'
-    )
-    parser.add_argument(
-        '--xedaproject',
-        default='xedaproject.toml',
-        help='Path to Xeda project file. By default will use xedaproject.toml in the current directory.'
-    )
-    parser.add_argument('--override-settings', nargs='+',
-                        help=('Override setting value. Use <hierarchy>.key=value format'
-                              'example: --override-settings flows.vivado_run.stop_time=100us'
-                              )
-                        )
-    parser.add_argument('--override-flow-settings', nargs='+',
-                        help=(
-                            'Override setting values for the specified main flow. Use <hierarchy>.key=value format'
-                            'example: xeda vivado_sim --override-settings stop_time=100us')
-                        )
-    parser.add_argument(
-        '--version',  action='version', version=f'%(prog)s {__version__}', help='Print version information and exit',
-    )
-
-    shtab.add_argument_to(parser, ["--print-completion"])
-    return parser
-
-
-def gen_shell_completion():
-    print("Installing shell completion")
-    parser = get_main_argparser()
-    completion = shtab.complete(parser, shell="bash")
-    xdg_home = os.environ.get('XDG_DATA_HOME', os.path.join(
-        os.environ.get('HOME', str(Path.home())), '.local', 'share'))
-    completion_user_dir = os.environ.get(
-        'BASH_COMPLETION_USER_DIR', os.path.join(xdg_home, 'bash-completion'))
-
-    dir = Path(completion_user_dir)
-    if not dir.exists():
-        dir.mkdir(parents=True)
-    completion_file = dir / 'xeda'
-    with open(completion_file, 'w') as f:
-        f.write(completion)
-    eager_completion = Path.home() / '.bash_completion'
-    source_line = f'. {completion_file} # added by xeda'
-    if eager_completion.exists():
-        with open(eager_completion, "r+") as f:
-            for line in f:
-                if source_line in line:
-                    break
-            else:  # else-for "completion clause"
-                f.write(source_line + os.linesep)
-    else:
-        with open(eager_completion, "w") as f:
-            f.write(source_line + os.linesep)
-
-
-def sanitize_toml(obj):
-    if isinstance(obj, (str, int, float, bool)):
-        return obj
-    elif isinstance(obj, list):
-        return [sanitize_toml(x) for x in obj]
-    elif isinstance(obj, tuple):
-        return tuple(sanitize_toml(list(obj)))
-    elif isinstance(obj, dict):
-        return {k: sanitize_toml(v) for k, v in obj.items()}
-    elif hasattr(obj, '__dict__'):
-        return(sanitize_toml(dict(**obj.__dict__)))
-    else:
-        print(
-            f"ERROR in xeda_app.sanitize_toml: unhandled object of type {type(obj)}: {obj}")
-        return sanitize_toml(dict(obj))
-
-def load_xedaproject(project_file: Path):
-    try:
-        with open(project_file) as f:
-            ext = project_file.suffix.lower()
-            if ext == '.json':
-                return json.load(f)
-            elif ext == '.toml':
-                return sanitize_toml(toml.load(f))
-            else:
-                exit(f"xedaproject: {project_file} has unknown extension {ext}. Currently supported formats are TOML (.toml) and JSON (.json)")
-    except FileNotFoundError:
-        exit(
-            f'Cannot open project file: {project_file}. Please run from the project directory with xedaproject.toml or specify the correct path using the --xedaproject flag')
-    except IsADirectoryError:
-        exit(f'The specified xedaproject is not a regular file.')
-
-class XedaApp:
-    def main(self, args=None):
-        parsed_args = get_main_argparser().parse_args(args)
-
-        if parsed_args.debug:
-            logger.setLevel(logging.DEBUG)
-
-        runner_cls = parsed_args.flow_runner
-
-        toml_path = Path(parsed_args.xedaproject)
-        xeda_project = load_xedaproject(toml_path)
-
-        if parsed_args.xeda_run_dir is None:
-            rundir = None
-            project = xeda_project.get('project')
-            if isinstance(project, list):
-                project = project[0]
-            if project:
-                rundir = project.get('xeda_run_dir')
-            if not rundir:
-                rundir = os.environ.get('xeda_run_dir')
-            if not rundir:
-                rundir = 'xeda_run'
-            parsed_args.xeda_run_dir = rundir
-
-        xeda_run_dir = Path(parsed_args.xeda_run_dir).resolve()
-        xeda_run_dir.mkdir(exist_ok=True, parents=True)
-
-        logdir = xeda_run_dir / 'Logs'
-        logdir.mkdir(exist_ok=True, parents=True)
-
-        timestamp = datetime.now().strftime("%Y-%m-%d-%H%M%S%f")[:-3]
-        logFormatter = logging.Formatter(
-            "%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s]  %(message)s")
-
-        logfile = logdir / f"xeda_{timestamp}.log"
-        print(f"Logging to {logfile}")
-
-        fileHandler = logging.FileHandler(logfile)
-        fileHandler.setFormatter(logFormatter)
-        logger.addHandler(fileHandler)
-
-        coloredlogs.install(
-            'INFO', fmt='%(asctime)s %(levelname)s %(message)s', logger=logger)
-
-        logger.info(f"Running using FlowRunner: {runner_cls.__name__}")
-
-        xeda_project['xeda_version'] = __version__
-
-        runner = runner_cls(parsed_args, xeda_project, timestamp)
-
-        runner.launch()


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 00000000..75c7c716
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,39 @@
+name: CI
+on:
+  push:
+    branches: ["master", "dev"]
+  pull_request:
+    branches: ["master", "dev"]
+  workflow_dispatch:
+
+jobs:
+  tox:
+    strategy:
+      matrix:
+        python-version: ["3.8", "3.9", "3.10"]
+
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: ghdl/setup-ghdl-ci@nightly
+        with:
+          backend: llvm
+
+      - uses: actions/checkout@v2
+        with:
+          submodules: true
+          fetch-depth: 0
+
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: "${{ matrix.python-version }}"
+
+      - name: Install dependencies
+        run: |
+          python -VV
+          python -m pip install --upgrade pip
+          python -m pip install --upgrade tox tox-gh-actions
+
+      - name: "Run tox targets for ${{ matrix.python-version }}"
+        run: "python -m tox"
diff --git a/examples/boards/ulx3s/dvi_test/blink.vhd b/examples/boards/ulx3s/dvi_test/blink.vhd
new file mode 100644
index 00000000..de98eac8
--- /dev/null
+++ b/examples/boards/ulx3s/dvi_test/blink.vhd
@@ -0,0 +1,29 @@
+-- (c)EMARD
+-- License=BSD
+
+library IEEE;
+use IEEE.STD_LOGIC_1164.ALL;
+
+entity blink is
+  generic
+  (
+    bits: integer := 23
+  );
+  port
+  (
+    clk: in std_logic;  -- main clock input from 25MHz clock source
+    led: out std_logic_vector(7 downto 0)
+  );
+end;
+
+architecture Behavioral of blink is
+    signal R_blink: std_logic_vector(bits-1 downto 0);
+begin
+    process(clk)
+    begin
+      if rising_edge(clk) then
+        R_blink <= R_blink+1;
+      end if;
+    end process;
+    led <= R_blink(R_blink'high downto R_blink'high-7);
+end Behavioral;
diff --git a/examples/boards/ulx3s/dvi_test/ecp5pll.vhdl b/examples/boards/ulx3s/dvi_test/ecp5pll.vhdl
new file mode 120000
index 00000000..70a9b783
--- /dev/null
+++ b/examples/boards/ulx3s/dvi_test/ecp5pll.vhdl
@@ -0,0 +1,1 @@
+../blinky/ecp5pll.vhdl
\ No newline at end of file
diff --git a/examples/boards/ulx3s/dvi_test/gen_pll.py b/examples/boards/ulx3s/dvi_test/gen_pll.py
new file mode 100755
index 00000000..267261de
--- /dev/null
+++ b/examples/boards/ulx3s/dvi_test/gen_pll.py
@@ -0,0 +1,12 @@
+#!/usr/bin/env python3
+import logging
+from xeda.flows.nextpnr import EcpPLL
+
+log = logging.getLogger(__name__)
+
+log.root.setLevel(logging.DEBUG)
+
+f_pixel = 25.0
+
+if __name__ == "__main__":
+    EcpPLL(clkouts=[600]).generate() # type: ignore
diff --git a/examples/boards/ulx3s/dvi_test/tmds_encoder.vhd b/examples/boards/ulx3s/dvi_test/tmds_encoder.vhd
new file mode 100644
index 00000000..7ce83fc8
--- /dev/null
+++ b/examples/boards/ulx3s/dvi_test/tmds_encoder.vhd
@@ -0,0 +1,117 @@
+----------------------------------------------------------------------------------
+-- Description: TMDS Encoder 
+--     8 bits colour, 2 control bits and one blanking bits in
+--       10 bits of TMDS encoded data out
+--     Clocked at the pixel clock
+--
+----------------------------------------------------------------------------------
+-- Copyright (c) 2012 Mike Field <hamster@snap.net.nz>
+-- Copyright (c) 2022 Kamyar Mohajerani <kammoh@gmail.com>
+-- 
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a copy
+-- of this software and associated documentation files (the "Software"), to deal
+-- in the Software without restriction, including without limitation the rights
+-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+-- copies of the Software, and to permit persons to whom the Software is
+-- furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in
+-- all copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+-- THE SOFTWARE.
+--
+
+library ieee;
+use ieee.std_logic_1164.all;
+use ieee.numeric_std.all;
+use ieee.numeric_std_unsigned.all;
+
+use work.utils_pkg.all;
+
+entity tmds_encoder is
+   Port(
+      clk     : in  std_logic;
+      data    : in  std_logic_vector(7 downto 0);
+      c       : in  std_logic_vector(1 downto 0);
+      blank   : in  std_logic;
+      encoded : out std_logic_vector(9 downto 0)
+   );
+end tmds_encoder;
+
+architecture Behavioral of tmds_encoder is
+   -- Registers
+   signal encoded_reg : std_logic_vector(encoded'range);
+   signal dc_bias     : unsigned(3 downto 0) := (others => '0');
+
+   -- Wires
+   signal xored, xnored       : std_logic_vector(8 downto 0);
+   signal num_ones            : unsigned(3 downto 0);
+   signal data_word           : std_logic_vector(8 downto 0);
+   signal data_word_inv       : std_logic_vector(8 downto 0);
+   signal data_word_disparity : unsigned(3 downto 0);
+begin
+   encoded <= encoded_reg;
+
+   -- Work our the two different encodings for the byte
+   xored(0)  <= data(0);
+   xored(8)  <= '1';
+   xnored(0) <= data(0);
+   xnored(8) <= '0';
+
+   GEN_BYTE_ENCODE : for i in 1 to 7 generate
+      xored(i)  <= data(i) xor xored(i - 1);
+      xnored(i) <= data(i) xnor xnored(i - 1);
+   end generate GEN_BYTE_ENCODE;
+
+   -- Count how many ones are set in data
+   num_ones <= sum_bits(data);
+
+   -- Decide which encoding to use
+   data_word     <= xnored when num_ones > 4 or (num_ones = 4 and data(0) = '0') else
+                    xored;
+   data_word_inv <= not data_word;
+
+   -- Work out the DC bias of the dataword;
+   data_word_disparity <= "1100" + sum_bits(data_word(7 downto 0));
+
+   -- Now work out what the output should be
+   process(clk)
+   begin
+      if rising_edge(clk) then
+         if blank = '1' then
+            -- In the control periods, all values have and have balanced bit count
+            case c is
+               when "00"   => encoded_reg <= "1101010100";
+               when "01"   => encoded_reg <= "0010101011";
+               when "10"   => encoded_reg <= "0101010100";
+               when others => encoded_reg <= "1010101011";
+            end case;
+            dc_bias <= (others => '0');
+         else
+            if dc_bias = "00000" or data_word_disparity = 0 then
+               -- dataword has no disparity
+               if data_word(8) = '1' then
+                  encoded_reg <= "01" & data_word(7 downto 0);
+                  dc_bias     <= dc_bias + data_word_disparity;
+               else
+                  encoded_reg <= "10" & data_word_inv(7 downto 0);
+                  dc_bias     <= dc_bias - data_word_disparity;
+               end if;
+            elsif (dc_bias(3) = '0' and data_word_disparity(3) = '0') or (dc_bias(3) = '1' and data_word_disparity(3) = '1') then
+               encoded_reg <= '1' & data_word(8) & data_word_inv(7 downto 0);
+               dc_bias     <= dc_bias + data_word(8) - data_word_disparity;
+            else
+               encoded_reg <= '0' & data_word;
+               dc_bias     <= dc_bias - data_word_inv(8) + data_word_disparity;
+            end if;
+         end if;
+      end if;
+   end process;
+end Behavioral;
diff --git a/examples/boards/ulx3s/dvi_test/top_vgatest.vhd b/examples/boards/ulx3s/dvi_test/top_vgatest.vhd
new file mode 100644
index 00000000..3078fd5c
--- /dev/null
+++ b/examples/boards/ulx3s/dvi_test/top_vgatest.vhd
@@ -0,0 +1,203 @@
+-- (c)EMARD
+-- License=BSD
+-- edited by Kamyar
+
+library ieee;
+use ieee.std_logic_1164.all;
+use ieee.numeric_std.all;
+use ieee.numeric_std_unsigned.all;
+
+entity top_vgatest is
+  generic(
+    --  640x400  @50Hz
+    --  640x400  @60Hz
+    --  640x480  @50Hz
+    --  640x480  @60Hz
+    --  720x576  @50Hz
+    --  720x576  @60Hz
+    --  800x480  @60Hz
+    --  800x600  @60Hz
+    -- 1024x768  @60Hz
+    -- 1280x768  @60Hz
+    -- 1366x768  @60Hz
+    -- 1280x1024 @60Hz
+    -- 1920x1080 @30Hz
+    -- 1920x1080 @50Hz overclock 540MHz
+    -- 1920x1200 @30Hz overclock 375MHz
+    -- 1920x1200 @50Hz overclock 600MHz
+    G_X        : positive := 1920;      -- pixels
+    G_Y        : positive := 1200;      -- pixels
+    G_FREQ     : positive := 30;        -- Hz 60,50,30
+    G_X_ADJUST : integer  := 0;         -- adjust -3..3 if no picture or to fine-tune G_FREQ
+    G_Y_ADJUST : integer  := 0;         -- adjust -3..3 if no picture or to fine-tune G_FREQ
+    G_EXT_GPDI : boolean  := FALSE;     -- FALSE:disable, TRUE:enable external gpdi
+    G_DDR      : boolean  := TRUE;      -- FALSE:SDR, TRUE:DDR
+    G_C_DEPTH  : positive := 8          -- color depth (bits)
+  );
+  port(
+    -- main clock input from 25MHz clock source
+    clk_25mhz : in    std_logic;
+    -- LEDs (active high)
+    led       : out   std_logic_vector(7 downto 0);
+    -- buttons (active high)
+    btn       : in    std_logic_vector(6 downto 0);
+    -- GPIO (some are shared with wifi and adc)
+    gp        : inout std_logic_vector(27 downto 0) := (others => 'Z');
+    gn        : inout std_logic_vector(27 downto 0) := (others => 'Z');
+    -- Digital Video (differential outputs)
+    gpdi_dp   : out   std_logic_vector(3 downto 0)
+  );
+end;
+
+architecture Behavioral of top_vgatest is
+  type T_NATURAL_ARRAY is array (natural range <>) of natural;
+  constant c_possible_freqs : T_NATURAL_ARRAY := (
+    25000000,
+    27000000,
+    40000000,
+    50000000,
+    54000000,
+    60000000,
+    65000000,
+    75000000,
+    80000000,                           -- overclock 400MHz
+    100000000,                          -- overclock 500MHz
+    108000000,                          -- overclock 540MHz
+    120000000                           -- overclock 600MHz
+  );
+
+  function F_find_next_f(f : natural) return natural is
+    variable f0 : natural := 0;
+  begin
+    for fx in c_possible_freqs'range loop
+      f0 := c_possible_freqs(fx);
+      if f0 >= f then
+        return f0;
+      end if;
+    end loop;
+    assert FALSE report "cant find next f for " & integer'image(f) severity failure;
+  end F_find_next_f;
+
+  --! Returns the number of bits required to represet values less than n (0 to n - 1 inclusive)
+  function log2ceil(n : natural) return natural is
+    variable r : natural := 0;
+  begin
+    while n > 2 ** r loop
+      r := r + 1;
+    end loop;
+    return r;
+  end function;
+
+  constant xminblank   : natural := G_X / 64; -- initial estimate
+  constant yminblank   : natural := G_Y / 64; -- for minimal blank space
+  constant min_pixel_f : natural := G_FREQ * (G_X + xminblank) * (G_Y + yminblank);
+  constant pixel_f     : natural := F_find_next_f(min_pixel_f);
+  constant yframe      : natural := G_Y + yminblank;
+  constant xframe      : natural := pixel_f / (G_FREQ * yframe);
+  constant xblank      : integer := xframe - G_X;
+  constant yblank      : integer := yframe - G_Y;
+
+  signal clk_pixel, clk_shift                        : std_logic;
+  signal vga_hsync, vga_vsync, vga_blank             : std_logic;
+  signal vga_r, vga_g, vga_b                         : std_logic_vector(G_C_DEPTH - 1 downto 0);
+  signal dvid_red, dvid_green, dvid_blue, dvid_clock : std_logic_vector(1 downto 0);
+
+  component ODDRX1F
+    generic(
+      GSR : string := "ENABLED"
+    );
+    port(
+      SCLK : in  std_logic;
+      RST  : in  std_logic;
+      D0   : in  std_logic;
+      D1   : in  std_logic;
+      Q    : out std_logic
+    );
+  end component;
+
+begin
+  assert false report "pixel_f=" & integer'image(pixel_f) severity note;
+
+  clk_single_pll : entity work.ecp5pll
+    generic map(
+      G_IN_HZ        => 25_000_000,
+      G_OUT_0_HZ     => pixel_f * 5,
+      G_OUT_0_TOL_HZ => 1_666_666,
+      G_OUT_1_HZ     => pixel_f,
+      G_OUT_1_TOL_HZ => 333333
+    )
+    port map(
+      clk_i   => clk_25mhz,
+      clk_o_0 => clk_shift,
+      clk_o_1 => clk_pixel
+    );
+
+  vga_instance : entity work.vga
+    generic map(
+      G_X_RES             => G_X,
+      c_hsync_front_porch => xblank / 3,
+      c_hsync_pulse       => xblank / 3,
+      c_hsync_back_porch  => xblank - 2 * (xblank / 3) + G_X_ADJUST,
+      G_Y_RES             => G_Y,
+      c_vsync_front_porch => yblank / 3,
+      c_vsync_pulse       => yblank / 3,
+      c_vsync_back_porch  => yblank - 2 * (yblank / 3) + G_Y_ADJUST,
+      G_X_BITS            => log2ceil(G_X + G_X_ADJUST + xblank), --12,
+      G_Y_BITS            => log2ceil(G_Y + G_Y_ADJUST + yblank),
+      G_DEPTH             => G_C_DEPTH
+    )
+    port map(
+      clk_pixel     => clk_pixel,
+      clk_pixel_ena => '1',
+      vga_r         => vga_r,
+      vga_g         => vga_g,
+      vga_b         => vga_b,
+      vga_hsync     => vga_hsync,
+      vga_vsync     => vga_vsync,
+      vga_blank     => vga_blank
+    );
+
+  led <= (
+    0      => vga_hsync,
+    1      => vga_vsync,
+    7      => vga_blank,
+    others => '0'
+  );
+
+  vga2dvid_instance : entity work.vga2dvid
+    generic map(
+      c_ddr                      => G_DDR,
+      c_shift_clock_synchronizer => FALSE,
+      c_depth                    => G_C_DEPTH
+    )
+    port map(
+      clk_pixel => clk_pixel,
+      clk_shift => clk_shift,
+      in_red    => vga_r,
+      in_green  => vga_g,
+      in_blue   => vga_b,
+      in_hsync  => vga_hsync,
+      in_vsync  => vga_vsync,
+      in_blank  => vga_blank,
+      -- single-ended output ready for differential buffers
+      out_red   => dvid_red,
+      out_green => dvid_green,
+      out_blue  => dvid_blue,
+      out_clock => dvid_clock
+    );
+
+  -- vendor specific DDR modules
+  -- convert SDR 2-bit input to DDR clocked 1-bit output (single-ended)
+  ddr_clock : ODDRX1F port map(D0 => dvid_clock(0), D1 => dvid_clock(1), Q => gpdi_dp(3), SCLK => clk_shift, RST => '0');
+  ddr_red : ODDRX1F port map(D0 => dvid_red(0), D1 => dvid_red(1), Q => gpdi_dp(2), SCLK => clk_shift, RST => '0');
+  ddr_green : ODDRX1F port map(D0 => dvid_green(0), D1 => dvid_green(1), Q => gpdi_dp(1), SCLK => clk_shift, RST => '0');
+  ddr_blue : ODDRX1F port map(D0 => dvid_blue(0), D1 => dvid_blue(1), Q => gpdi_dp(0), SCLK => clk_shift, RST => '0');
+
+  g_external_gpdi : if G_EXT_GPDI generate
+    ddr_xclock : ODDRX1F port map(D0 => dvid_clock(0), D1 => dvid_clock(1), Q => gp(12), SCLK => clk_shift, RST => '0');
+    ddr_xred : ODDRX1F port map(D0 => dvid_red(0), D1 => dvid_red(1), Q => gp(11), SCLK => clk_shift, RST => '0');
+    ddr_xgreen : ODDRX1F port map(D0 => dvid_green(0), D1 => dvid_green(1), Q => gp(10), SCLK => clk_shift, RST => '0');
+    ddr_xblue : ODDRX1F port map(D0 => dvid_blue(0), D1 => dvid_blue(1), Q => gp(9), SCLK => clk_shift, RST => '0');
+  end generate;
+
+end Behavioral;
diff --git a/examples/boards/ulx3s/dvi_test/utils_pkg.vhdl b/examples/boards/ulx3s/dvi_test/utils_pkg.vhdl
new file mode 100644
index 00000000..a1aa8f8e
--- /dev/null
+++ b/examples/boards/ulx3s/dvi_test/utils_pkg.vhdl
@@ -0,0 +1,49 @@
+--===============================================================================================--
+--! @file              utils_pkg.vhdl
+--! @brief             Utility package
+--! @author            Kamyar Mohajerani
+--! @copyright         Copyright (c) 2022 Kamyar Mohajerani
+--!
+--! @license           Solderpad Hardware License v2.1 ([SHL-2.1](https://solderpad.org/licenses/SHL-2.1/))
+--! @vhdl              VHDL 2008 and later
+--!
+--! @details           collection of utility functions, procedures, type definitions, etc.
+--!
+--===============================================================================================--
+
+library ieee;
+use ieee.std_logic_1164.all;
+use ieee.numeric_std.all;
+
+package utils_pkg is
+    -- types
+    type T_UNSIGNED_ARRAY is array (natural range <>) of unsigned;
+
+    -- functions
+    function log2ceil(n : natural) return natural;
+    function sum_bits(slv : std_logic_vector) return unsigned;
+
+end package;
+
+package body utils_pkg is
+    --! Returns the number of bits required to represet values less than n (0 to n - 1 inclusive)
+    function log2ceil(n : natural) return natural is
+        variable r : natural := 0;
+    begin
+        while n > 2 ** r loop
+            r := r + 1;
+        end loop;
+        return r;
+    end function;
+
+    --! Returns the sum of all bits in `slv`
+    function sum_bits(slv : std_logic_vector) return unsigned is
+        variable sum : unsigned(log2ceil(slv'length + 1) - 1 downto 0) := (others => '0');
+    begin
+        for i in slv'range loop
+            sum := sum + slv(i);
+        end loop;
+        return sum;
+    end function;
+
+end package body;
diff --git a/examples/boards/ulx3s/dvi_test/vga.vhd b/examples/boards/ulx3s/dvi_test/vga.vhd
new file mode 100644
index 00000000..201974fd
--- /dev/null
+++ b/examples/boards/ulx3s/dvi_test/vga.vhd
@@ -0,0 +1,184 @@
+-- AUTHOR=EMARD
+-- LICENSE=BSD
+--
+-- Generates VGA picture from sequential bitmap data from pixel clock synchronous FIFO.
+-- signal 'fetch_next' is set high for 1 clk_pixel period as soon as current pixel data is consumed
+-- fifo should be fast enough to fetch new data for new pixel
+--
+-- modified by Kamyar
+
+LIBRARY ieee;
+USE ieee.std_logic_1164.all;
+use ieee.numeric_std.all;
+
+entity vga is
+  generic(
+    G_X_RES             : integer  := 640;
+    c_hsync_front_porch : integer  := 16;
+    c_hsync_pulse       : integer  := 96;
+    c_hsync_back_porch  : integer  := 44; -- 48
+    G_Y_RES             : integer  := 480;
+    c_vsync_front_porch : integer  := 10;
+    c_vsync_pulse       : integer  := 2;
+    c_vsync_back_porch  : integer  := 31; -- 33
+    G_X_BITS            : integer  := 10; -- should fit c_resolution_x + c_hsync_front_porch + c_hsync_pulse + c_hsync_back_porch
+    G_Y_BITS            : integer  := 10; -- should fit c_resolution_y + c_vsync_front_porch + c_vsync_pulse + c_vsync_back_porch
+    G_DEPTH             : positive := 8
+    -- c_dbl_x             : integer :=   0; -- 0-normal X, 1-double X
+    -- c_dbl_y             : integer :=   0  -- 0-normal X, 1-double X
+  );
+  port(
+    clk_pixel                     : in  std_logic; -- pixel clock, 25 MHz for 640x480
+    clk_pixel_ena                 : in  std_logic := '1'; -- pixel clock ena
+    -- test_picture                  : in  std_logic                    := '0'; -- '1' to show test picture
+    fetch_next                    : out std_logic; -- request FIFO to fetch next pixel data
+    beam_x                        : out std_logic_vector(G_X_BITS - 1 downto 0);
+    beam_y                        : out std_logic_vector(G_Y_BITS - 1 downto 0);
+    -- r_i, g_i, b_i                 : in  std_logic_vector(c_depth - 1 downto 0) := (others => '0'); -- pixel data from FIFO
+    vga_r, vga_g, vga_b           : out std_logic_vector(G_DEPTH - 1 downto 0); -- 8-bit VGA video signal out
+    vga_hsync, vga_vsync          : out std_logic; -- VGA sync
+    vga_vblank, vga_blank, vga_de : out std_logic -- V blank for CPU interrupts and H+V blank for digital encoder (HDMI)
+  );
+end vga;
+
+architecture RTL of vga is
+  signal x_counter : unsigned(G_X_BITS - 1 downto 0); -- (9 downto 0) is good for up to 1023 frame timing width (resolution 640x480)
+  signal y_counter : unsigned(G_Y_BITS - 1 downto 0); -- (9 downto 0) is good for up to 1023 frame timing width (resolution 640x480)
+
+  constant c_hblank_on                     : unsigned(x_counter'range) := to_unsigned(G_X_RES - 1, G_X_BITS);
+  constant c_hsync_on                      : unsigned(x_counter'range) := to_unsigned(G_X_RES + c_hsync_front_porch - 1, G_X_BITS);
+  constant c_hsync_off                     : unsigned(x_counter'range) := to_unsigned(G_X_RES + c_hsync_front_porch + c_hsync_pulse - 1, G_X_BITS);
+  constant c_hblank_off                    : unsigned(x_counter'range) := to_unsigned(G_X_RES + c_hsync_front_porch + c_hsync_pulse + c_hsync_back_porch - 1, G_X_BITS);
+  constant c_frame_x                       : unsigned(x_counter'range) := to_unsigned(G_X_RES + c_hsync_front_porch + c_hsync_pulse + c_hsync_back_porch - 1, G_X_BITS);
+  -- frame_x = 640 + 16 + 96 + 48 = 800;
+  constant c_vblank_on                     : unsigned(y_counter'range) := to_unsigned(G_Y_RES - 1, G_Y_BITS);
+  constant c_vsync_on                      : unsigned(y_counter'range) := to_unsigned(G_Y_RES + c_vsync_front_porch - 1, G_Y_BITS);
+  constant c_vsync_off                     : unsigned(y_counter'range) := to_unsigned(G_Y_RES + c_vsync_front_porch + c_vsync_pulse - 1, G_Y_BITS);
+  constant c_vblank_off                    : unsigned(y_counter'range) := to_unsigned(G_Y_RES + c_vsync_front_porch + c_vsync_pulse + c_vsync_back_porch - 1, G_Y_BITS);
+  constant c_frame_y                       : unsigned(y_counter'range) := to_unsigned(G_Y_RES + c_vsync_front_porch + c_vsync_pulse + c_vsync_back_porch - 1, G_Y_BITS);
+  -- frame_y = 480 + 10 + 2 + 33 = 525;
+  -- refresh_rate = pixel_clock/(frame_x*frame_y) = 25MHz / (800*525) = 59.52Hz
+  signal R_hsync, R_vsync, R_blank, R_disp : std_logic; -- disp = not blank
+  signal R_disp_early, R_vdisp             : std_logic; -- blank generation
+  signal R_blank_early, R_vblank           : std_logic; -- blank generation
+  signal R_fetch_next                      : std_logic;
+  signal R_vga_r, R_vga_g, R_vga_b         : std_logic_vector(G_DEPTH - 1 downto 0);
+  -- test picture generation
+  signal W, A, T                           : std_logic_vector(G_DEPTH - 1 downto 0);
+  signal Z                                 : std_logic_vector(G_DEPTH - 3 downto 0);
+
+begin
+
+  assert FALSE report "VGA parameters:" & --
+  LF & "    G_X_RES  : " & integer'image(G_X_RES) & --
+  LF & "    G_Y_RES  : " & integer'image(G_Y_RES) & --
+  LF & "    G_X_BITS : " & integer'image(G_X_BITS) & --
+  LF & "    G_Y_BITS : " & integer'image(G_Y_BITS) & --
+  LF & "    G_DEPTH  : " & integer'image(G_DEPTH) & --
+  LF severity NOTE;
+
+  process(clk_pixel)
+  begin
+    if rising_edge(clk_pixel) then
+      if clk_pixel_ena = '1' then
+        if x_counter = c_frame_x then
+          x_counter <= (others => '0');
+          if y_counter = c_frame_y then
+            y_counter <= (others => '0');
+          else
+            y_counter <= y_counter + 1;
+          end if;
+        else
+          x_counter <= x_counter + 1;
+        end if;
+        R_fetch_next <= R_disp_early;
+      else
+        R_fetch_next <= '0';
+      end if;
+    end if;
+  end process;
+
+  beam_x <= std_logic_vector(x_counter);
+  beam_y <= std_logic_vector(y_counter);
+
+  fetch_next <= R_fetch_next;
+
+  -- generate sync and blank
+  process(clk_pixel)
+  begin
+    if rising_edge(clk_pixel) then
+      if x_counter = c_hblank_on then
+        R_blank_early <= '1';
+        R_disp_early  <= '0';
+      elsif x_counter = c_hblank_off then
+        R_blank_early <= R_vblank;      -- "OR" function
+        R_disp_early  <= R_vdisp;       -- "AND" function
+      end if;
+    end if;
+  end process;
+  process(clk_pixel)
+  begin
+    if rising_edge(clk_pixel) then
+      if x_counter = c_hsync_on then
+        R_hsync <= '1';
+      elsif x_counter = c_hsync_off then
+        R_hsync <= '0';
+      end if;
+    end if;
+  end process;
+  process(clk_pixel)
+  begin
+    if rising_edge(clk_pixel) then
+      if y_counter = c_vblank_on then
+        R_vblank <= '1';
+        R_vdisp  <= '0';
+      elsif y_counter = c_vblank_off then
+        R_vblank <= '0';
+        R_vdisp  <= '1';
+      end if;
+    end if;
+  end process;
+  process(clk_pixel)
+  begin
+    if rising_edge(clk_pixel) then
+      if y_counter = c_vsync_on then
+        R_vsync <= '1';
+      elsif y_counter = c_vsync_off then
+        R_vsync <= '0';
+      end if;
+    end if;
+  end process;
+
+  -- test picture generator
+  A <= (others => '1') when std_logic_vector(x_counter(7 downto 5)) = "010" and std_logic_vector(y_counter(7 downto 5)) = "010" else (others => '0');
+  W <= (others => '1') when x_counter(7 downto 0) = y_counter(7 downto 0) else (others => '0');
+  Z <= (others => '1') when std_logic_vector(y_counter(4 downto 3)) = not std_logic_vector(x_counter(4 downto 3)) else (others => '0');
+  T <= (others => y_counter(6));
+  process(clk_pixel)
+  begin
+    if rising_edge(clk_pixel) then
+      if R_blank = '1' then
+        -- analog VGA needs this, DVI doesn't
+        R_vga_r <= (others => '0');
+        R_vga_g <= (others => '0');
+        R_vga_b <= (others => '0');
+      else
+        R_vga_r <= (((std_logic_vector(x_counter(G_DEPTH - 3 downto 0)) and Z) & "00") or W) and not A;
+        R_vga_g <= ((std_logic_vector(x_counter(G_DEPTH - 1 downto 0)) and T) or W) and not A;
+        R_vga_b <= std_logic_vector(y_counter(G_DEPTH - 1 downto 0)) or W or A;
+      end if;
+      R_blank <= R_blank_early;
+      R_disp  <= R_disp_early;
+    end if;
+  end process;
+
+  vga_r      <= R_vga_r;
+  vga_g      <= R_vga_g;
+  vga_b      <= R_vga_b;
+  vga_hsync  <= R_hsync;
+  vga_vsync  <= R_vsync;
+  vga_blank  <= R_blank;
+  vga_vblank <= R_vblank;
+  vga_de     <= R_disp;
+
+end RTL;
diff --git a/examples/boards/ulx3s/dvi_test/vga2dvid.vhd b/examples/boards/ulx3s/dvi_test/vga2dvid.vhd
new file mode 100644
index 00000000..74c44037
--- /dev/null
+++ b/examples/boards/ulx3s/dvi_test/vga2dvid.vhd
@@ -0,0 +1,229 @@
+--------------------------------------------------------------------------------
+-- Engineer:    Mike Field <hamster@snap.net.nz>
+-- Description: Converts VGA signals into DVID bitstreams.
+--
+--    'clk_shift' 10x clk_pixel for SDR
+--    'clk_shift'  5x clk_pixel for DDR
+--
+--    'blank' should be asserted during the non-display 
+--    portions of the frame
+--------------------------------------------------------------------------------
+-- See: http://hamsterworks.co.nz/mediawiki/index.php/Dvid_test
+--      http://hamsterworks.co.nz/mediawiki/index.php/FPGA_Projects
+--
+-- Copyright (c) 2012 Mike Field <hamster@snap.net.nz>
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a copy
+-- of this software and associated documentation files (the "Software"), to deal
+-- in the Software without restriction, including without limitation the rights
+-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+-- copies of the Software, and to permit persons to whom the Software is
+-- furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in
+-- all copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+-- THE SOFTWARE.
+--
+
+-- takes VGA input and prepares output
+
+-- for SDR buffer, which send 1 bit per 1 clock period output out_red(0), out_green(0), ... etc.
+-- for DDR buffers, which send 2 bits per 1 clock period output out_red(1 downto 0), ...
+
+-- EMARD unified SDR and DDR into one module
+
+library ieee;
+use ieee.std_logic_1164.all;
+use IEEE.numeric_std.ALL;
+
+entity vga2dvid is
+    Generic(
+        c_shift_clock_synchronizer : boolean := TRUE; -- try to get out_clock in sync with clk_pixel
+        c_parallel                 : boolean := TRUE; -- default output parallel data
+        c_serial                   : boolean := TRUE; -- default output serial data
+        c_ddr                      : boolean := FALSE; -- default use SDR for serial data
+        c_depth                    : integer := 8
+    );
+    Port(
+        clk_pixel                       : in  std_logic; -- VGA pixel clock, 25 MHz for 640x480
+        clk_shift                       : in  std_logic; -- SDR: 10x clk_pixel, DDR: 5x clk_pixel, in phase with clk_pixel
+        in_red                          : in  std_logic_vector(c_depth - 1 downto 0);
+        in_green                        : in  std_logic_vector(c_depth - 1 downto 0);
+        in_blue                         : in  std_logic_vector(c_depth - 1 downto 0);
+        in_blank                        : in  std_logic;
+        in_hsync                        : in  std_logic;
+        in_vsync                        : in  std_logic;
+        -- parallel outputs
+        outp_red, outp_green, outp_blue : out std_logic_vector(9 downto 0);
+        -- serial outputs
+        out_red                         : out std_logic_vector(1 downto 0);
+        out_green                       : out std_logic_vector(1 downto 0);
+        out_blue                        : out std_logic_vector(1 downto 0);
+        out_clock                       : out std_logic_vector(1 downto 0)
+    );
+end vga2dvid;
+
+architecture Behavioral of vga2dvid is
+    signal encoded_red, encoded_green, encoded_blue : std_logic_vector(9 downto 0);
+    signal latched_red, latched_green, latched_blue : std_logic_vector(9 downto 0) := (others => '0');
+    signal shift_red, shift_green, shift_blue       : std_logic_vector(9 downto 0) := (others => '0');
+    constant c_shift_clock_initial                  : std_logic_vector(9 downto 0) := "0000011111";
+    signal shift_clock                              : std_logic_vector(9 downto 0) := c_shift_clock_initial;
+    signal R_shift_clock_off_sync                   : std_logic                    := '0';
+    signal R_shift_clock_synchronizer               : unsigned(7 downto 0)         := (others => '0');
+    signal R_sync_fail                              : unsigned(6 downto 0); -- counts sync fails, after too many, reinitialize shift_clock
+
+    constant c_red   : std_logic_vector(1 downto 0) := (others => '0');
+    constant c_green : std_logic_vector(1 downto 0) := (others => '0');
+    signal c_blue    : std_logic_vector(1 downto 0);
+
+    signal red_d   : std_logic_vector(7 downto 0);
+    signal green_d : std_logic_vector(7 downto 0);
+    signal blue_d  : std_logic_vector(7 downto 0);
+begin
+    c_blue <= in_vsync & in_hsync;
+
+    red_d(7 downto 8 - c_depth)   <= in_red(c_depth - 1 downto 0);
+    green_d(7 downto 8 - c_depth) <= in_green(c_depth - 1 downto 0);
+    blue_d(7 downto 8 - c_depth)  <= in_blue(c_depth - 1 downto 0);
+
+    -- fill vacant low bits with value repeated (so min/max value is always 0 or 255)
+    G_vacant_bits : if c_depth < 8 generate
+        G_bits : for i in 0 to 8 - c_depth - 1 generate
+            red_d(i)   <= in_red(0);
+            green_d(i) <= in_green(0);
+            blue_d(i)  <= in_blue(0);
+        end generate;
+    end generate;
+
+    G_shift_clock_synchronizer : if c_shift_clock_synchronizer generate
+        -- sampler verifies is shift_clock state synchronous with pixel_clock
+        process(clk_pixel)
+        begin
+            if rising_edge(clk_pixel) then
+                -- does 0 to 1 transition at bits 5 downto 4 happen at rising_edge of clk_pixel?
+                -- if shift_clock = c_shift_clock_initial then
+                if shift_clock(5 downto 4) = c_shift_clock_initial(5 downto 4) then -- same as above line but simplified
+                    R_shift_clock_off_sync <= '0';
+                else
+                    R_shift_clock_off_sync <= '1';
+                end if;
+            end if;
+        end process;
+        -- every N cycles of clk_shift: signal to skip 1 cycle in order to get in sync
+        process(clk_shift)
+        begin
+            if rising_edge(clk_shift) then
+                if R_shift_clock_off_sync = '1' then
+                    if R_shift_clock_synchronizer(R_shift_clock_synchronizer'high) = '1' then
+                        R_shift_clock_synchronizer <= (others => '0');
+                    else
+                        R_shift_clock_synchronizer <= R_shift_clock_synchronizer + 1;
+                    end if;
+                else
+                    R_shift_clock_synchronizer <= (others => '0');
+                end if;
+            end if;
+        end process;
+    end generate;                       -- shift_clock_synchronizer
+
+    u21 : entity work.tmds_encoder
+        PORT MAP(clk => clk_pixel, data => red_d, c => c_red, blank => in_blank, encoded => encoded_red);
+    u22 : entity work.tmds_encoder
+        PORT MAP(clk => clk_pixel, data => green_d, c => c_green, blank => in_blank, encoded => encoded_green);
+    u23 : entity work.tmds_encoder
+        PORT MAP(clk => clk_pixel, data => blue_d, c => c_blue, blank => in_blank, encoded => encoded_blue);
+
+    process(clk_pixel)
+    begin
+        if rising_edge(clk_pixel) then
+            latched_red   <= encoded_red;
+            latched_green <= encoded_green;
+            latched_blue  <= encoded_blue;
+        end if;
+    end process;
+
+    G_parallel : if c_parallel generate
+        outp_red   <= latched_red;
+        outp_green <= latched_green;
+        outp_blue  <= latched_blue;
+    end generate;
+
+    G_SDR : if c_serial and not c_ddr generate
+        process(clk_shift)
+        begin
+            if rising_edge(clk_shift) then
+                --if shift_clock = "0000011111" then
+                if shift_clock(5 downto 4) = c_shift_clock_initial(5 downto 4) then -- same as above line but simplified
+                    shift_red   <= latched_red;
+                    shift_green <= latched_green;
+                    shift_blue  <= latched_blue;
+                else
+                    shift_red   <= "0" & shift_red(9 downto 1);
+                    shift_green <= "0" & shift_green(9 downto 1);
+                    shift_blue  <= "0" & shift_blue(9 downto 1);
+                end if;
+                if R_shift_clock_synchronizer(R_shift_clock_synchronizer'high) = '0' then
+                    shift_clock <= shift_clock(0) & shift_clock(9 downto 1);
+                else
+                    -- synchronization failed.
+                    -- after too many fails, reinitialize shift_clock
+                    if R_sync_fail(R_sync_fail'high) = '1' then
+                        shift_clock <= c_shift_clock_initial;
+                        R_sync_fail <= (others => '0');
+                    else
+                        R_sync_fail <= R_sync_fail + 1;
+                    end if;
+                end if;
+            end if;
+        end process;
+    end generate;
+
+    G_DDR : if c_serial and c_ddr generate
+        process(clk_shift)
+        begin
+            if rising_edge(clk_shift) then
+                --if shift_clock = "0000011111" then
+                if shift_clock(5 downto 4) = c_shift_clock_initial(5 downto 4) then -- same as above line but simplified
+                    shift_red   <= latched_red;
+                    shift_green <= latched_green;
+                    shift_blue  <= latched_blue;
+                else
+                    shift_red   <= "00" & shift_red(9 downto 2);
+                    shift_green <= "00" & shift_green(9 downto 2);
+                    shift_blue  <= "00" & shift_blue(9 downto 2);
+                end if;
+                if R_shift_clock_synchronizer(R_shift_clock_synchronizer'high) = '0' then
+                    shift_clock <= shift_clock(1 downto 0) & shift_clock(9 downto 2);
+                else
+                    -- synchronization failed.
+                    -- after too many fails, reinitialize shift_clock
+                    if R_sync_fail(R_sync_fail'high) = '1' then
+                        shift_clock <= c_shift_clock_initial;
+                        R_sync_fail <= (others => '0');
+                    else
+                        R_sync_fail <= R_sync_fail + 1;
+                    end if;
+                end if;
+            end if;
+        end process;
+    end generate;
+
+    -- SDR: use only bit 0 from each out_* channel 
+    -- DDR: 2 bits per 1 clock period,
+    -- (one bit output on rising edge, other on falling edge of clk_shift)
+    G_serial : if c_serial generate
+        out_red   <= shift_red(1 downto 0);
+        out_green <= shift_green(1 downto 0);
+        out_blue  <= shift_blue(1 downto 0);
+        out_clock <= shift_clock(1 downto 0);
+    end generate;
+
+end Behavioral;
diff --git a/examples/boards/ulx3s/dvi_test/xedaproject.toml b/examples/boards/ulx3s/dvi_test/xedaproject.toml
new file mode 100644
index 00000000..8385547e
--- /dev/null
+++ b/examples/boards/ulx3s/dvi_test/xedaproject.toml
@@ -0,0 +1,30 @@
+[design]
+
+name = "dvi_test"
+
+[design.language]
+vhdl.standard = 2008
+
+[design.rtl]
+sources = [
+    "utils_pkg.vhdl",
+    "vga.vhd",
+    "vga2dvid.vhd",
+    "tmds_encoder.vhd",
+    "ecp5pll.vhdl",
+    "top_vgatest.vhd",
+]
+top = "top_vgatest"
+
+clock_port = "clk_25mhz"
+
+[flows.nextpnr]
+board = "ULX3S_85F"
+clock_period = 40   # 25 MHz
+detailed_timing_report = false # nextpnr BUG, o/w crashes
+
+[flows.openfpgaloader]
+board = "ULX3S_85F"
+clock_period = 40   # 25 MHz
+nextpnr.detailed_timing_report = false
+nextpnr.timing_allow_fail = true
diff --git a/examples/vhdl/sqrt/test.py b/examples/vhdl/sqrt/test.py
new file mode 100755
index 00000000..2ae164fb
--- /dev/null
+++ b/examples/vhdl/sqrt/test.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+
+import logging
+import os
+from pathlib import Path
+
+from xeda import Design, Flow
+from xeda.flow_runner import DefaultRunner
+from xeda.flows import GhdlSim, Yosys
+from xeda.tool import ExecutableNotFound
+
+log = logging.getLogger(__name__)
+
+SCRIPT_DIR = Path(__file__).parent.resolve()
+
+design = Design.from_toml(SCRIPT_DIR / "sqrt.toml")
+xeda_runner = DefaultRunner()
+
+
+def test_sqrt() -> None:
+    LONG_TEST = [8, 9, 16, 17, 18, 20, 21, 31, 32, 63, 64, 66]
+    QUICK_TEST = [8, 32]
+    ws = QUICK_TEST
+    if os.environ.get("LONG_TEST"):
+        print("runing long test!")
+        ws = LONG_TEST
+    for w in ws:
+        assert design.tb
+        design.tb.parameters = {**design.tb.parameters, "G_IN_WIDTH": w}
+        os.environ["NUM_TV"] = str(100)
+        f: Flow = xeda_runner.run_flow(GhdlSim, design)
+        if not f.succeeded:
+            raise AssertionError(f"test failed for w={w}")
+
+
+def test_sqrt_yosys_synth() -> None:
+    design.tb.parameters["G_IN_WIDTH"] = 32
+    try:
+        f = xeda_runner.run_flow(
+            Yosys, design, {"fpga": {"part": "LFE5U-25F-6BG381C"}, "clock_period": 10.0}
+        )
+        assert f.succeeded
+    except ExecutableNotFound:
+        log.critical("Yosys executable not found")
+
+
+if __name__ == "__main__":
+    test_sqrt()
+    test_sqrt_yosys_synth()
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 00000000..0db50ffc
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,4 @@
+from pathlib import Path
+
+TESTS_DIR = Path(__file__).parent.absolute()
+RESOURCES_DIR = TESTS_DIR / "resources"
diff --git a/tests/fake_tools/fake_tool.py b/tests/fake_tools/fake_tool.py
new file mode 100755
index 00000000..40c042cf
--- /dev/null
+++ b/tests/fake_tools/fake_tool.py
@@ -0,0 +1,198 @@
+#!/usr/bin/env python3
+
+import inspect
+import logging
+import os
+from pathlib import Path
+from time import sleep
+from typing import (
+    Any,
+    Callable,
+    Dict,
+    List,
+    Optional,
+    Protocol,
+    Union,
+    runtime_checkable,
+)
+from zipfile import ZipFile
+
+import click
+from xeda.utils import XedaBaseModel
+
+log = logging.getLogger()
+
+RESOURCE_DIR = Path(__file__).parent.absolute() / "resource"
+
+
+def write_file(path, data):
+    path.parent.mkdir(parents=True, exist_ok=True)
+    if isinstance(data, bytes):
+        with open(path, "wb") as f:
+            f.write(data)
+    else:
+        if data is None:
+            data = []
+        with open(path, "w") as f:
+            if isinstance(data, list):
+                f.writelines(data)
+            else:
+                f.write(data)
+
+
+@runtime_checkable
+class Executer(Protocol):
+    def __call__(self, **kwargs: Any) -> int:
+        ...
+
+
+class WriteFile(Executer):
+    def __init__(
+        self,
+        path: Union[str, os.PathLike],
+        data: Union[None, List[str], str, bytes] = None,
+        **kwargs: Any,
+    ) -> None:
+        if not isinstance(path, Path):
+            path = Path(path)
+        self.path = path
+        self.data = data
+        super().__init__(**kwargs)
+
+    def __call__(self, **kwargs) -> int:
+        write_file(self.path, self.data)
+        return 0
+
+
+class TouchFiles(Executer):
+    def __init__(self, *paths: Union[str, os.PathLike], **kwargs) -> None:
+        self.paths = paths
+        super().__init__(**kwargs)
+
+    def __call__(self, **kwargs) -> int:
+        for path in self.paths:
+            path = Path(path)
+            path.parent.mkdir(parents=True, exist_ok=True)
+            path.touch(exist_ok=True)
+        return 0
+
+
+class FakeTool(XedaBaseModel):
+    version: Optional[str] = None
+    version_template: Optional[str] = None
+    vendor: Optional[str] = None
+    help_options: list = ["--help"]
+    version_options: list = ["--version"]
+    options: dict = {}  # param_decls -> attrs
+    arguments: dict = {}  # Dict[str, Optional[Dict[str, Any]]] = {}
+    execute_: Executer = lambda **_kwargs: 0
+
+    @property
+    def version_banner(self) -> str:
+        if self.version_template:
+            return inspect.cleandoc(self.version_template.format(**(self.dict())))
+        return "unknown"
+
+    def execute(self, **kwargs) -> int:
+        return self.execute_(**kwargs)
+
+
+class FakeVivado(FakeTool):
+    vendor = "Xilinx, Inc."
+    version = "v2021.2"
+    version_template = """Vivado {version} (64-bit)
+        SW Build 3367213 on Tue Oct 19 02:47:39 MDT 2021
+        IP Build 3369179 on Thu Oct 21 08:25:16 MDT 2021
+        Copyright 1986-2021 {vendor} All Rights Reserved.
+    """
+    help_options = ["-help"]
+    version_options = ["-version"]
+    options = {
+        "-mode": ["gui", "tcl", "batch"],
+        "-init": dict(type=click.Path(exists=True)),
+        "-source": dict(type=click.Path(exists=True)),
+        "-verbose": None,
+        "-nojournal": None,
+        "-notrace": None,
+        "-nolog": None,
+    }
+    arguments = {"project": dict(required=False)}
+
+    def execute(self, **kwargs):
+        print("cwd =", Path.cwd())
+        tcl = kwargs.get("source")
+        if tcl:
+            sleep(0.3)
+            with ZipFile(RESOURCE_DIR / "fake_vivado_reports") as zf:
+                for file in zf.namelist():
+                    if os.path.isdir(file):
+                        continue
+                    with zf.open(file) as rf:
+                        data = rf.read()
+                        write_file(Path("reports") / "route_design" / file, data)
+
+
+fake_tools: Dict[str, FakeTool] = dict(
+    vivado=FakeVivado(),  # type: ignore
+    quartus_sh=FakeTool(
+        options={"-t": dict(type=click.Path(exists=True), required=True)},
+        execute_=TouchFiles(
+            "reports/Flow_Summary.csv",
+            "reports/Fitter/Resource_Section/Fitter_Resource_Utilization_by_Entity.csv",
+            "reports/Timing_Analyzer/Multicorner_Timing_Analysis_Summary.csv",
+        ),
+    ),
+    xtclsh=FakeTool(
+        arguments={"script": dict(required=False, type=click.Path(exists=True))}
+    ),
+)
+
+symlink_name = Path(__file__).stem
+
+tool = fake_tools.get(symlink_name, FakeTool())
+
+
+FC = Callable[..., Any]
+
+
+def fake_tool_options(fake_tool: Optional[FakeTool]) -> FC:
+    def decorator(f: FC) -> FC:
+        if fake_tool:
+            f = click.group(
+                invoke_without_command=True,
+                context_settings=dict(help_option_names=fake_tool.help_options),
+            )(f)
+            f = click.version_option(
+                fake_tool.version,
+                *fake_tool.version_options,
+                message=fake_tool.version_banner,
+            )(f)
+            for arg, attrs in fake_tool.arguments.items():
+                if attrs is None:
+                    attrs = {}
+                f = click.argument(arg, **attrs)(f)
+            for param_decls, param_attrs in fake_tool.options.items():
+                if isinstance(param_decls, str):
+                    param_decls = (param_decls,)
+                if param_attrs is None:
+                    param_attrs = dict(is_flag=True)
+                elif isinstance(param_attrs, list):
+                    param_attrs = dict(type=click.Choice(param_attrs))
+                elif isinstance(param_attrs, type):
+                    param_attrs = dict(type=param_attrs)
+                f = click.option(*param_decls, **param_attrs)(f)
+        return f
+
+    return decorator
+
+
+@fake_tool_options(tool)
+@click.pass_context
+def cli(ctx: click.Context, **kwargs):
+    if tool:
+        print(f"Fake {ctx.info_name} kwargs:{kwargs} args:{ctx.args}")
+        tool.execute(**kwargs)
+
+
+if __name__ == "__main__":
+    cli()  # pylint: disable=no-value-for-parameter
diff --git a/tests/fake_tools/quartus_sh b/tests/fake_tools/quartus_sh
new file mode 120000
index 00000000..d6fa627c
--- /dev/null
+++ b/tests/fake_tools/quartus_sh
@@ -0,0 +1,1 @@
+fake_tool.py
\ No newline at end of file
diff --git a/tests/fake_tools/resource/fake_vivado_reports b/tests/fake_tools/resource/fake_vivado_reports
new file mode 100644
index 00000000..494dd7dd
Binary files /dev/null and b/tests/fake_tools/resource/fake_vivado_reports differ
diff --git a/tests/fake_tools/vivado b/tests/fake_tools/vivado
new file mode 120000
index 00000000..d6fa627c
--- /dev/null
+++ b/tests/fake_tools/vivado
@@ -0,0 +1,1 @@
+fake_tool.py
\ No newline at end of file
diff --git a/tests/fake_tools/xtclsh b/tests/fake_tools/xtclsh
new file mode 120000
index 00000000..d6fa627c
--- /dev/null
+++ b/tests/fake_tools/xtclsh
@@ -0,0 +1,1 @@
+fake_tool.py
\ No newline at end of file
diff --git a/tests/resources/Fitter_Resource_Utilization_by_Entity.csv b/tests/resources/Fitter_Resource_Utilization_by_Entity.csv
new file mode 100644
index 00000000..ba88b947
--- /dev/null
+++ b/tests/resources/Fitter_Resource_Utilization_by_Entity.csv
@@ -0,0 +1,2 @@
+Compilation Hierarchy Node,ALMs needed [=A-B+C],[A] ALMs used in final placement,[B] Estimate of ALMs recoverable by dense packing,[C] Estimate of ALMs unavailable,ALMs used for memory,Combinational ALUTs,Dedicated Logic Registers,I/O Registers,Block Memory Bits,M10Ks,DSP Blocks,Pins,Virtual Pins,Full Hierarchy Name,Entity Name,Library Name
+|full_adder_piped,1.5 (1.5),1.5 (1.5),0.0 (0.0),0.0 (0.0),0.0 (0.0),3 (3),2 (2),0 (0),0,0,0,7,0,|full_adder_piped,full_adder_piped,work
diff --git a/tests/resources/Flow_Summary.csv b/tests/resources/Flow_Summary.csv
new file mode 100644
index 00000000..857db6b7
--- /dev/null
+++ b/tests/resources/Flow_Summary.csv
@@ -0,0 +1,19 @@
+Flow Status,Successful - Tue Mar  1 11:10:35 2022
+Quartus Prime Version,21.1.0 Build 842 10/21/2021 SJ Lite Edition
+Revision Name,pipelined_adder
+Top-level Entity Name,full_adder_piped
+Family,Cyclone V
+Device,5CGXBC3B6F23C7
+Timing Models,Final
+Logic utilization (in ALMs),2 / 13,460 ( < 1 % )
+Total registers,2
+Total pins,7 / 222 ( 3 % )
+Total virtual pins,0
+Total block memory bits,0 / 1,382,400 ( 0 % )
+Total DSP Blocks,0 / 57 ( 0 % )
+Total HSSI RX PCSs,0 / 3 ( 0 % )
+Total HSSI PMA RX Deserializers,0 / 3 ( 0 % )
+Total HSSI TX PCSs,0 / 3 ( 0 % )
+Total HSSI PMA TX Serializers,0 / 3 ( 0 % )
+Total PLLs,0 / 7 ( 0 % )
+Total DLLs,0 / 3 ( 0 % )
diff --git a/tests/resources/cocotb/results.xml b/tests/resources/cocotb/results.xml
new file mode 100644
index 00000000..ff5b1973
--- /dev/null
+++ b/tests/resources/cocotb/results.xml
@@ -0,0 +1,8 @@
+<testsuites name="results">
+  <testsuite name="all" package="all">
+    <property name="random_seed" value="1646240102" />
+    <testcase name="test_sqrt" classname="tb_sqrt" file="/Users/kamyar/src/xeda/examples/vhdl/sqrt/tb/tb_sqrt.py" lineno="7" time="0.002543926239013672" sim_time_ns="200.000001" ratio_time="78618.6320706939">
+      <failure />
+    </testcase>
+  </testsuite>
+</testsuites>
diff --git a/tests/resources/design0/design0.toml b/tests/resources/design0/design0.toml
new file mode 100644
index 00000000..af50d4f0
--- /dev/null
+++ b/tests/resources/design0/design0.toml
@@ -0,0 +1,14 @@
+name = 'design0'
+description = 'test'
+language.vhdl.standard = "2008"
+
+[rtl]
+sources = ['../test.v'] # by default relative paths are relative to the location of the TOML file, unless overridden by the "design_root" field
+top = 'design0'
+clock_port = 'clk'
+parameters = { G_IN_WIDTH = 32, G_ITERATIVE = true, G_STR= "abcd", G_BITVECTOR="7'b0101001" }
+
+# [tb]
+# sources = []
+# cocotb = true
+# # top = 'tb_sqrt'
diff --git a/tests/resources/test.v b/tests/resources/test.v
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/resources/wave.opt b/tests/resources/wave.opt
new file mode 100644
index 00000000..ad84956d
--- /dev/null
+++ b/tests/resources/wave.opt
@@ -0,0 +1,368 @@
+$ version 1.1
+
+# Signals in packages :
+
+# Signals in entities :
+/lwc_tb/stop_clock
+/lwc_tb/reset_done
+/lwc_tb/clk
+/lwc_tb/rst
+/lwc_tb/pdi_data
+/lwc_tb/pdi_data_delayed
+/lwc_tb/pdi_valid
+/lwc_tb/pdi_valid_delayed
+/lwc_tb/pdi_ready
+/lwc_tb/sdi_data
+/lwc_tb/sdi_data_delayed
+/lwc_tb/sdi_valid
+/lwc_tb/sdi_valid_delayed
+/lwc_tb/sdi_ready
+/lwc_tb/do_data
+/lwc_tb/do_valid
+/lwc_tb/do_last
+/lwc_tb/do_ready
+/lwc_tb/do_ready_delayed
+/lwc_tb/rdi_data
+/lwc_tb/rdi_data_delayed
+/lwc_tb/rdi_valid
+/lwc_tb/rdi_valid_delayed
+/lwc_tb/rdi_ready
+/lwc_tb/pdi_operation_count
+/lwc_tb/cycle_counter
+/lwc_tb/idle_counter
+/lwc_tb/rdi_counter
+/lwc_tb/rdi_count0
+/lwc_tb/start_cycle
+/lwc_tb/timing_active
+/lwc_tb/stop_timing
+/lwc_tb/uut/clk
+/lwc_tb/uut/rst
+/lwc_tb/uut/pdi_data
+/lwc_tb/uut/pdi_valid
+/lwc_tb/uut/pdi_ready
+/lwc_tb/uut/sdi_data
+/lwc_tb/uut/sdi_valid
+/lwc_tb/uut/sdi_ready
+/lwc_tb/uut/do_data
+/lwc_tb/uut/do_last
+/lwc_tb/uut/do_valid
+/lwc_tb/uut/do_ready
+/lwc_tb/uut/key_cipher_in
+/lwc_tb/uut/key_valid_cipher_in
+/lwc_tb/uut/key_ready_cipher_in
+/lwc_tb/uut/bdi_cipher_in
+/lwc_tb/uut/bdi_valid_cipher_in
+/lwc_tb/uut/bdi_ready_cipher_in
+/lwc_tb/uut/bdi_pad_loc_cipher_in
+/lwc_tb/uut/bdi_valid_bytes_cipher_in
+/lwc_tb/uut/bdi_size_cipher_in
+/lwc_tb/uut/bdi_eot_cipher_in
+/lwc_tb/uut/bdi_eoi_cipher_in
+/lwc_tb/uut/bdi_type_cipher_in
+/lwc_tb/uut/decrypt_cipher_in
+/lwc_tb/uut/hash_cipher_in
+/lwc_tb/uut/key_update_cipher_in
+/lwc_tb/uut/bdo_cipher_out
+/lwc_tb/uut/bdo_valid_cipher_out
+/lwc_tb/uut/bdo_ready_cipher_out
+/lwc_tb/uut/bdo_last_cipher_out
+/lwc_tb/uut/bdo_valid_bytes_cipher_out
+/lwc_tb/uut/bdo_type_cipher_out
+/lwc_tb/uut/msg_auth_valid
+/lwc_tb/uut/msg_auth_ready
+/lwc_tb/uut/msg_auth
+/lwc_tb/uut/cmd_fifo_in
+/lwc_tb/uut/cmd_valid_fifo_in
+/lwc_tb/uut/cmd_ready_fifo_in
+/lwc_tb/uut/cmd_fifo_out
+/lwc_tb/uut/cmd_valid_fifo_out
+/lwc_tb/uut/cmd_ready_fifo_out
+/lwc_tb/uut/do_fifo_in_valid
+/lwc_tb/uut/do_fifo_in_ready
+/lwc_tb/uut/do_fifo_in_data
+/lwc_tb/uut/do_fifo_in_last
+/lwc_tb/uut/do_fifo_in
+/lwc_tb/uut/do_fifo_out
+/lwc_tb/uut/inst_preprocessor/clk
+/lwc_tb/uut/inst_preprocessor/rst
+/lwc_tb/uut/inst_preprocessor/pdi_data
+/lwc_tb/uut/inst_preprocessor/pdi_valid
+/lwc_tb/uut/inst_preprocessor/pdi_ready
+/lwc_tb/uut/inst_preprocessor/sdi_data
+/lwc_tb/uut/inst_preprocessor/sdi_valid
+/lwc_tb/uut/inst_preprocessor/sdi_ready
+/lwc_tb/uut/inst_preprocessor/key_data
+/lwc_tb/uut/inst_preprocessor/key_valid
+/lwc_tb/uut/inst_preprocessor/key_ready
+/lwc_tb/uut/inst_preprocessor/key_update
+/lwc_tb/uut/inst_preprocessor/bdi_data
+/lwc_tb/uut/inst_preprocessor/bdi_valid_bytes
+/lwc_tb/uut/inst_preprocessor/bdi_pad_loc
+/lwc_tb/uut/inst_preprocessor/bdi_size
+/lwc_tb/uut/inst_preprocessor/bdi_eot
+/lwc_tb/uut/inst_preprocessor/bdi_eoi
+/lwc_tb/uut/inst_preprocessor/bdi_type
+/lwc_tb/uut/inst_preprocessor/bdi_valid
+/lwc_tb/uut/inst_preprocessor/bdi_ready
+/lwc_tb/uut/inst_preprocessor/decrypt
+/lwc_tb/uut/inst_preprocessor/hash
+/lwc_tb/uut/inst_preprocessor/cmd_data
+/lwc_tb/uut/inst_preprocessor/cmd_valid
+/lwc_tb/uut/inst_preprocessor/cmd_ready
+/lwc_tb/uut/inst_preprocessor/state
+/lwc_tb/uut/inst_preprocessor/seglen_counter
+/lwc_tb/uut/inst_preprocessor/eoi_flag
+/lwc_tb/uut/inst_preprocessor/eot_flag
+/lwc_tb/uut/inst_preprocessor/last_flag
+/lwc_tb/uut/inst_preprocessor/decrypt_op
+/lwc_tb/uut/inst_preprocessor/hash_op
+/lwc_tb/uut/inst_preprocessor/hdr_type
+/lwc_tb/uut/inst_preprocessor/bdi_type_p
+/lwc_tb/uut/inst_preprocessor/bdi_type_s
+/lwc_tb/uut/inst_preprocessor/bdi_eoi_p
+/lwc_tb/uut/inst_preprocessor/bdi_eot_p
+/lwc_tb/uut/inst_preprocessor/bdi_eoi_s
+/lwc_tb/uut/inst_preprocessor/bdi_eot_s
+/lwc_tb/uut/inst_preprocessor/pdi_ready_o
+/lwc_tb/uut/inst_preprocessor/sdi_ready_o
+/lwc_tb/uut/inst_preprocessor/bdi_valid_p
+/lwc_tb/uut/inst_preprocessor/bdi_ready_p
+/lwc_tb/uut/inst_preprocessor/key_valid_p
+/lwc_tb/uut/inst_preprocessor/key_ready_p
+/lwc_tb/uut/inst_preprocessor/key_update_p
+/lwc_tb/uut/inst_preprocessor/hash_s
+/lwc_tb/uut/inst_preprocessor/decrypt_s
+/lwc_tb/uut/inst_preprocessor/key_valid_s
+/lwc_tb/uut/inst_preprocessor/bdi_last_s
+/lwc_tb/uut/inst_preprocessor/bdi_valid_s
+/lwc_tb/uut/inst_preprocessor/bdi_size_p
+/lwc_tb/uut/inst_preprocessor/bdi_size_s
+/lwc_tb/uut/inst_preprocessor/op_is_actkey
+/lwc_tb/uut/inst_preprocessor/op_is_hash
+/lwc_tb/uut/inst_preprocessor/hdr_first
+/lwc_tb/uut/inst_preprocessor/hdr_last
+/lwc_tb/uut/inst_preprocessor/reading_pdi_hdr
+/lwc_tb/uut/inst_preprocessor/reading_sdi_hdr
+/lwc_tb/uut/inst_preprocessor/pdi_fire
+/lwc_tb/uut/inst_preprocessor/sdi_fire
+/lwc_tb/uut/inst_preprocessor/seglen
+/lwc_tb/uut/inst_preprocessor/hdr_seglen
+/lwc_tb/uut/inst_preprocessor/seglen_is_zero
+/lwc_tb/uut/inst_preprocessor/last_flit_of_segment
+/lwc_tb/uut/inst_preprocessor/relay_hdr_to_postproc
+/lwc_tb/uut/inst_preprocessor/bdi_valid_bytes_p
+/lwc_tb/uut/inst_preprocessor/bdi_pad_loc_p
+/lwc_tb/uut/inst_preprocessor/bdi_valid_bytes_s
+/lwc_tb/uut/inst_preprocessor/bdi_pad_loc_s
+/lwc_tb/uut/inst_preprocessor/nx_state
+/lwc_tb/uut/inst_preprocessor/cur_hdr_last
+/lwc_tb/uut/inst_preprocessor/received_wrong_header
+/lwc_tb/uut/inst_preprocessor/keypiso/clk
+/lwc_tb/uut/inst_preprocessor/keypiso/rst
+/lwc_tb/uut/inst_preprocessor/keypiso/p_in_data
+/lwc_tb/uut/inst_preprocessor/keypiso/p_in_keep
+/lwc_tb/uut/inst_preprocessor/keypiso/p_in_last
+/lwc_tb/uut/inst_preprocessor/keypiso/p_in_valid
+/lwc_tb/uut/inst_preprocessor/keypiso/p_in_ready
+/lwc_tb/uut/inst_preprocessor/keypiso/s_out_data
+/lwc_tb/uut/inst_preprocessor/keypiso/s_out_keep
+/lwc_tb/uut/inst_preprocessor/keypiso/s_out_last
+/lwc_tb/uut/inst_preprocessor/keypiso/s_out_valid
+/lwc_tb/uut/inst_preprocessor/keypiso/s_out_ready
+/lwc_tb/uut/inst_preprocessor/bdipiso/clk
+/lwc_tb/uut/inst_preprocessor/bdipiso/rst
+/lwc_tb/uut/inst_preprocessor/bdipiso/p_in_data
+/lwc_tb/uut/inst_preprocessor/bdipiso/p_in_keep
+/lwc_tb/uut/inst_preprocessor/bdipiso/p_in_last
+/lwc_tb/uut/inst_preprocessor/bdipiso/p_in_valid
+/lwc_tb/uut/inst_preprocessor/bdipiso/p_in_ready
+/lwc_tb/uut/inst_preprocessor/bdipiso/s_out_data
+/lwc_tb/uut/inst_preprocessor/bdipiso/s_out_keep
+/lwc_tb/uut/inst_preprocessor/bdipiso/s_out_last
+/lwc_tb/uut/inst_preprocessor/bdipiso/s_out_valid
+/lwc_tb/uut/inst_preprocessor/bdipiso/s_out_ready
+/lwc_tb/uut/inst_cryptocore/clk
+/lwc_tb/uut/inst_cryptocore/rst
+/lwc_tb/uut/inst_cryptocore/key
+/lwc_tb/uut/inst_cryptocore/key_valid
+/lwc_tb/uut/inst_cryptocore/key_ready
+/lwc_tb/uut/inst_cryptocore/key_update
+/lwc_tb/uut/inst_cryptocore/bdi
+/lwc_tb/uut/inst_cryptocore/bdi_pad_loc
+/lwc_tb/uut/inst_cryptocore/bdi_valid_bytes
+/lwc_tb/uut/inst_cryptocore/bdi_size
+/lwc_tb/uut/inst_cryptocore/bdi_eot
+/lwc_tb/uut/inst_cryptocore/bdi_eoi
+/lwc_tb/uut/inst_cryptocore/bdi_type
+/lwc_tb/uut/inst_cryptocore/bdi_valid
+/lwc_tb/uut/inst_cryptocore/bdi_ready
+/lwc_tb/uut/inst_cryptocore/decrypt_in
+/lwc_tb/uut/inst_cryptocore/hash_in
+/lwc_tb/uut/inst_cryptocore/bdo
+/lwc_tb/uut/inst_cryptocore/bdo_type
+/lwc_tb/uut/inst_cryptocore/bdo_valid_bytes
+/lwc_tb/uut/inst_cryptocore/end_of_block
+/lwc_tb/uut/inst_cryptocore/bdo_valid
+/lwc_tb/uut/inst_cryptocore/bdo_ready
+/lwc_tb/uut/inst_cryptocore/msg_auth
+/lwc_tb/uut/inst_cryptocore/msg_auth_valid
+/lwc_tb/uut/inst_cryptocore/msg_auth_ready
+/lwc_tb/uut/inst_cryptocore/tag_wen_s
+/lwc_tb/uut/inst_cryptocore/tag_addr_s
+/lwc_tb/uut/inst_cryptocore/tag_din_s
+/lwc_tb/uut/inst_cryptocore/tag_dout_s
+/lwc_tb/uut/inst_cryptocore/data_to_tag_s
+/lwc_tb/uut/inst_cryptocore/key_wen_s
+/lwc_tb/uut/inst_cryptocore/key_addr_s
+/lwc_tb/uut/inst_cryptocore/key_din_s
+/lwc_tb/uut/inst_cryptocore/key_dout_s
+/lwc_tb/uut/inst_cryptocore/npub_wen_s
+/lwc_tb/uut/inst_cryptocore/npub_addr_s
+/lwc_tb/uut/inst_cryptocore/npub_din_s
+/lwc_tb/uut/inst_cryptocore/npub_dout_s
+/lwc_tb/uut/inst_cryptocore/padd_npub_s
+/lwc_tb/uut/inst_cryptocore/n_state_s
+/lwc_tb/uut/inst_cryptocore/state_s
+/lwc_tb/uut/inst_cryptocore/len_s
+/lwc_tb/uut/inst_cryptocore/len_word_s
+/lwc_tb/uut/inst_cryptocore/ad_byte_cnt_s
+/lwc_tb/uut/inst_cryptocore/ad_byte_cnt_s_next
+/lwc_tb/uut/inst_cryptocore/ad_bit_cnt_vec_s
+/lwc_tb/uut/inst_cryptocore/msg_byte_cnt_s
+/lwc_tb/uut/inst_cryptocore/msg_byte_cnt_s_next
+/lwc_tb/uut/inst_cryptocore/msg_bit_cnt_vec_s
+/lwc_tb/uut/inst_cryptocore/block_cnt_s
+/lwc_tb/uut/inst_cryptocore/block_cnt_s_next
+/lwc_tb/uut/inst_cryptocore/block_num_s
+/lwc_tb/uut/inst_cryptocore/block_num_word_s
+/lwc_tb/uut/inst_cryptocore/word_cnt_s
+/lwc_tb/uut/inst_cryptocore/word_cnt_s_next
+/lwc_tb/uut/inst_cryptocore/ram_addr_s
+/lwc_tb/uut/inst_cryptocore/key_s
+/lwc_tb/uut/inst_cryptocore/key_ready_s
+/lwc_tb/uut/inst_cryptocore/bdi_ready_s
+/lwc_tb/uut/inst_cryptocore/bdi_s
+/lwc_tb/uut/inst_cryptocore/bdi_valid_bytes_s
+/lwc_tb/uut/inst_cryptocore/bdi_pad_loc_s
+/lwc_tb/uut/inst_cryptocore/bdo_s
+/lwc_tb/uut/inst_cryptocore/bdo_valid_bytes_s
+/lwc_tb/uut/inst_cryptocore/bdo_valid_s
+/lwc_tb/uut/inst_cryptocore/bdo_type_s
+/lwc_tb/uut/inst_cryptocore/end_of_block_s
+/lwc_tb/uut/inst_cryptocore/msg_auth_valid_s
+/lwc_tb/uut/inst_cryptocore/bdi_partial_s
+/lwc_tb/uut/inst_cryptocore/n_decrypt_s
+/lwc_tb/uut/inst_cryptocore/decrypt_s
+/lwc_tb/uut/inst_cryptocore/n_hash_s
+/lwc_tb/uut/inst_cryptocore/hash_s
+/lwc_tb/uut/inst_cryptocore/n_empty_hash_s
+/lwc_tb/uut/inst_cryptocore/empty_hash_s
+/lwc_tb/uut/inst_cryptocore/n_msg_auth_s
+/lwc_tb/uut/inst_cryptocore/msg_auth_s
+/lwc_tb/uut/inst_cryptocore/n_eoi_s
+/lwc_tb/uut/inst_cryptocore/eoi_s
+/lwc_tb/uut/inst_cryptocore/n_update_key_s
+/lwc_tb/uut/inst_cryptocore/update_key_s
+/lwc_tb/uut/inst_cryptocore/i_tag_ram/clk
+/lwc_tb/uut/inst_cryptocore/i_tag_ram/wen
+/lwc_tb/uut/inst_cryptocore/i_tag_ram/addr
+/lwc_tb/uut/inst_cryptocore/i_tag_ram/din
+/lwc_tb/uut/inst_cryptocore/i_tag_ram/dout
+/lwc_tb/uut/inst_cryptocore/i_tag_ram/ram
+/lwc_tb/uut/inst_cryptocore/i_key_ram/clk
+/lwc_tb/uut/inst_cryptocore/i_key_ram/wen
+/lwc_tb/uut/inst_cryptocore/i_key_ram/addr
+/lwc_tb/uut/inst_cryptocore/i_key_ram/din
+/lwc_tb/uut/inst_cryptocore/i_key_ram/dout
+/lwc_tb/uut/inst_cryptocore/i_key_ram/ram
+/lwc_tb/uut/inst_cryptocore/i_npub_ram/clk
+/lwc_tb/uut/inst_cryptocore/i_npub_ram/wen
+/lwc_tb/uut/inst_cryptocore/i_npub_ram/addr
+/lwc_tb/uut/inst_cryptocore/i_npub_ram/din
+/lwc_tb/uut/inst_cryptocore/i_npub_ram/dout
+/lwc_tb/uut/inst_cryptocore/i_npub_ram/ram
+/lwc_tb/uut/inst_postprocessor/clk
+/lwc_tb/uut/inst_postprocessor/rst
+/lwc_tb/uut/inst_postprocessor/bdo_data
+/lwc_tb/uut/inst_postprocessor/bdo_valid_bytes
+/lwc_tb/uut/inst_postprocessor/bdo_last
+/lwc_tb/uut/inst_postprocessor/bdo_type
+/lwc_tb/uut/inst_postprocessor/bdo_valid
+/lwc_tb/uut/inst_postprocessor/bdo_ready
+/lwc_tb/uut/inst_postprocessor/auth_success
+/lwc_tb/uut/inst_postprocessor/auth_valid
+/lwc_tb/uut/inst_postprocessor/auth_ready
+/lwc_tb/uut/inst_postprocessor/cmd_data
+/lwc_tb/uut/inst_postprocessor/cmd_valid
+/lwc_tb/uut/inst_postprocessor/cmd_ready
+/lwc_tb/uut/inst_postprocessor/do_data
+/lwc_tb/uut/inst_postprocessor/do_last
+/lwc_tb/uut/inst_postprocessor/do_valid
+/lwc_tb/uut/inst_postprocessor/do_ready
+/lwc_tb/uut/inst_postprocessor/state
+/lwc_tb/uut/inst_postprocessor/eot_flag
+/lwc_tb/uut/inst_postprocessor/decrypt_flag
+/lwc_tb/uut/inst_postprocessor/status_success
+/lwc_tb/uut/inst_postprocessor/seglen_counter
+/lwc_tb/uut/inst_postprocessor/nx_state
+/lwc_tb/uut/inst_postprocessor/cmd_ready_o
+/lwc_tb/uut/inst_postprocessor/do_valid_o
+/lwc_tb/uut/inst_postprocessor/bdo_valid_p
+/lwc_tb/uut/inst_postprocessor/bdo_ready_p
+/lwc_tb/uut/inst_postprocessor/bdo_last_p
+/lwc_tb/uut/inst_postprocessor/bdo_data_p
+/lwc_tb/uut/inst_postprocessor/seglen
+/lwc_tb/uut/inst_postprocessor/nx_decrypt
+/lwc_tb/uut/inst_postprocessor/nx_eot
+/lwc_tb/uut/inst_postprocessor/seglen_is_zero
+/lwc_tb/uut/inst_postprocessor/do_fire
+/lwc_tb/uut/inst_postprocessor/cmd_fire
+/lwc_tb/uut/inst_postprocessor/bdo_p_fire
+/lwc_tb/uut/inst_postprocessor/op_is_hash
+/lwc_tb/uut/inst_postprocessor/op_is_decrypt
+/lwc_tb/uut/inst_postprocessor/reset_hdr_counter
+/lwc_tb/uut/inst_postprocessor/hdr_first
+/lwc_tb/uut/inst_postprocessor/hdr_last
+/lwc_tb/uut/inst_postprocessor/sending_hdr
+/lwc_tb/uut/inst_postprocessor/last_flit_of_segment
+/lwc_tb/uut/inst_postprocessor/bdosipo/clk
+/lwc_tb/uut/inst_postprocessor/bdosipo/rst
+/lwc_tb/uut/inst_postprocessor/bdosipo/sin_data
+/lwc_tb/uut/inst_postprocessor/bdosipo/sin_keep
+/lwc_tb/uut/inst_postprocessor/bdosipo/sin_last
+/lwc_tb/uut/inst_postprocessor/bdosipo/sin_valid
+/lwc_tb/uut/inst_postprocessor/bdosipo/sin_ready
+/lwc_tb/uut/inst_postprocessor/bdosipo/pout_data
+/lwc_tb/uut/inst_postprocessor/bdosipo/pout_keep
+/lwc_tb/uut/inst_postprocessor/bdosipo/pout_last
+/lwc_tb/uut/inst_postprocessor/bdosipo/pout_valid
+/lwc_tb/uut/inst_postprocessor/bdosipo/pout_ready
+/lwc_tb/uut/inst_postprocessor/bdosipo/sin_data_clr
+/lwc_tb/uut/inst_headerfifo/clk
+/lwc_tb/uut/inst_headerfifo/rst
+/lwc_tb/uut/inst_headerfifo/din
+/lwc_tb/uut/inst_headerfifo/din_valid
+/lwc_tb/uut/inst_headerfifo/din_ready
+/lwc_tb/uut/inst_headerfifo/dout
+/lwc_tb/uut/inst_headerfifo/dout_valid
+/lwc_tb/uut/inst_headerfifo/dout_ready
+/lwc_tb/uut/inst_headerfifo/storage
+/lwc_tb/uut/inst_headerfifo/gen_depth_1/full
+/lwc_tb/uut/inst_headerfifo/gen_depth_1/dout_valid_o
+/lwc_tb/uut/inst_headerfifo/gen_depth_1/din_ready_o
+/lwc_tb/uut/inst_headerfifo/gen_depth_1/enq
+/lwc_tb/uut/inst_headerfifo/gen_depth_1/deq
+/lwc_tb/uut/inst_doutfifo/clk
+/lwc_tb/uut/inst_doutfifo/rst
+/lwc_tb/uut/inst_doutfifo/din
+/lwc_tb/uut/inst_doutfifo/din_valid
+/lwc_tb/uut/inst_doutfifo/din_ready
+/lwc_tb/uut/inst_doutfifo/dout
+/lwc_tb/uut/inst_doutfifo/dout_valid
+/lwc_tb/uut/inst_doutfifo/dout_ready
+/lwc_tb/uut/inst_doutfifo/storage
+/lwc_tb/uut/inst_doutfifo/gen_depth_1/full
+/lwc_tb/uut/inst_doutfifo/gen_depth_1/dout_valid_o
+/lwc_tb/uut/inst_doutfifo/gen_depth_1/din_ready_o
+/lwc_tb/uut/inst_doutfifo/gen_depth_1/enq
+/lwc_tb/uut/inst_doutfifo/gen_depth_1/deq
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100644
index 00000000..7a83c4e4
--- /dev/null
+++ b/tests/test_cli.py
@@ -0,0 +1,27 @@
+from click.testing import CliRunner
+from xeda.cli import cli
+
+flows = ["ghdl_sim", "nextpnr", "vivado_sim", "vivado_synth"]
+
+
+def test_cli_run_help():
+    runner = CliRunner()
+    result = runner.invoke(cli, ["run", "--help"])
+    assert result.exit_code == 0
+    assert "Usage: " in result.output
+
+
+def test_cli_list_flows():
+    runner = CliRunner()
+    result = runner.invoke(cli, ["list-flows"])
+    assert result.exit_code == 0
+    for flow in flows:
+        assert flow in result.output
+
+
+def test_cli_list_settings():
+    runner = CliRunner()
+    for flow_name in flows:
+        result = runner.invoke(cli, ["list-settings", flow_name])
+        assert result.exit_code == 0
+        # assert result.output.count("Usage: run") > 0
diff --git a/tests/test_cocotb.py b/tests/test_cocotb.py
new file mode 100644
index 00000000..47dbd18c
--- /dev/null
+++ b/tests/test_cocotb.py
@@ -0,0 +1,46 @@
+from pathlib import Path
+from typing import Any, Dict
+
+from xeda.flows.flow import Cocotb
+from xeda.utils import WorkingDirectory
+
+TESTS_DIR = Path(__file__).parent.absolute()
+RESOURCES_DIR = TESTS_DIR / "resources"
+
+
+def test_cocotb_version():
+    cocotb = Cocotb(sim_name="ghdl")  # type: ignore
+
+    assert cocotb.version_gte(0)
+    assert cocotb.version_gte(0, 0)
+    assert cocotb.version_gte(0, 1)
+    assert cocotb.version_gte(0, 1, 1)
+    assert cocotb.version_gte(0, 1, 1, 1)
+    assert cocotb.version_gte(1)
+    assert cocotb.version_gte(1, 5)
+    assert cocotb.version_gte(1, 6)
+    assert cocotb.version_gte(1, 6, 0)
+    assert cocotb.version_gte(1, 6, 1)
+    assert cocotb.version_gte(1, 6, 2)
+    assert not cocotb.version_gte(1, 7)
+    assert not cocotb.version_gte(1, 7, 0)
+    assert not cocotb.version_gte(2)
+    assert not cocotb.version_gte(2, 0, 0)
+    assert not cocotb.version_gte(2, 0, 0, 0)
+
+
+def test_cocotb_parse_xml():
+    assert (RESOURCES_DIR / "cocotb" / "results.xml").exists()
+    with WorkingDirectory(RESOURCES_DIR / "cocotb"):
+        cocotb = Cocotb(sim_name="dummy")  # type: ignore
+        for case in cocotb.result_testcases:
+            print(case)
+        results: Dict[str, Any] = {}
+        cocotb.add_results(results)
+        if not results["success"] is False:
+            raise AssertionError()
+
+
+if __name__ == "__main__":
+    test_cocotb_version()
+    test_cocotb_parse_xml()
diff --git a/tests/test_gtkw.py b/tests/test_gtkw.py
new file mode 100644
index 00000000..9b027bb0
--- /dev/null
+++ b/tests/test_gtkw.py
@@ -0,0 +1,32 @@
+from pathlib import Path
+from xeda.flows.ghdl import gen_gtkw, _get_wave_opt_signals, common_root
+
+
+TESTS_DIR = Path(__file__).parent.absolute()
+RESOURCES_DIR = TESTS_DIR / "resources"
+
+def test_gen_gtkw():
+    opt_file = RESOURCES_DIR / "wave.opt"
+    dump_file = "debug.ghw"
+    extra_top = "top"
+
+    signals, root_group = _get_wave_opt_signals(opt_file, extra_top)
+    assert len(signals) > 1
+    assert len(root_group) == 2
+    gen_gtkw(dump_file, signals, root_group)
+    gtkw = Path("debug.gtkw")
+    assert gtkw.exists()
+    with open(gtkw) as f:
+        lines = f.read().splitlines()
+    for s in signals:
+        sig_name = ".".join(s)
+        assert lines.count(sig_name) > 0 , f"{sig_name} not in lines"
+
+
+def test_common_root():
+    cr = common_root([[1, 2, 3, 4, 5], [1, 2, 3, 7], [1, 2, 3], [1, 2, 3, 9]])
+    assert cr == [1, 2, 3]
+
+
+if __name__ == "__main__":
+    test_gen_gtkw()
diff --git a/tests/test_ise.py b/tests/test_ise.py
new file mode 100644
index 00000000..710e1649
--- /dev/null
+++ b/tests/test_ise.py
@@ -0,0 +1,35 @@
+import os
+import tempfile
+from pathlib import Path
+
+from xeda import Design
+from xeda.flow_runner import DefaultRunner
+from xeda.flows import IseSynth
+from xeda.flows.flow import FPGA
+
+TESTS_DIR = Path(__file__).parent.absolute()
+RESOURCES_DIR = TESTS_DIR / "resources"
+EXAMPLES_DIR = TESTS_DIR.parent / "examples"
+
+os.environ["PATH"] += os.pathsep + os.path.join(TESTS_DIR, "fake_tools")
+
+
+def test_ise_synth_py() -> None:
+    path = RESOURCES_DIR / "design0/design0.toml"
+    # Append to PATH so if the actual tool exists, would take precedences.
+    assert path.exists()
+    design = Design.from_toml(EXAMPLES_DIR / "vhdl" / "sqrt" / "sqrt.toml")
+    settings = dict(fpga=FPGA("xc7a12tcsg325-1"), clock_period=5.5)
+    with tempfile.TemporaryDirectory() as run_dir:
+        print("Xeda run dir: ", run_dir)
+        xeda_runner = DefaultRunner(run_dir, debug=True)
+        flow = xeda_runner.run_flow(IseSynth, design, settings)
+        settings_json = flow.run_path / "settings.json"
+        results_json = flow.run_path / "results.json"
+        assert settings_json.exists()
+        assert results_json.exists()
+        # assert flow.succeeded
+
+
+if __name__ == "__main__":
+    test_ise_synth_py()
diff --git a/tests/test_quartus.py b/tests/test_quartus.py
new file mode 100644
index 00000000..518a3491
--- /dev/null
+++ b/tests/test_quartus.py
@@ -0,0 +1,142 @@
+"""test Intel Quartus flow"""
+import os
+import tempfile
+import logging
+from pathlib import Path
+
+from xeda import Design
+from xeda.flow_runner import DefaultRunner
+from xeda.flows import Quartus
+from xeda.flows.flow import FPGA
+from xeda.flows.quartus import parse_csv, try_num
+
+TESTS_DIR = Path(__file__).parent.absolute()
+RESOURCES_DIR = TESTS_DIR / "resources"
+EXAMPLES_DIR = TESTS_DIR.parent / "examples"
+
+
+log = logging.getLogger(__name__)
+
+log.root.setLevel(logging.DEBUG)
+log.setLevel(logging.DEBUG)
+
+
+def test_parse_csv():
+    resources = parse_csv(
+        RESOURCES_DIR / "Fitter_Resource_Utilization_by_Entity.csv",
+        id_field="Compilation Hierarchy Node",
+        field_parser=lambda s: try_num(s.split()[0]),
+        id_parser=lambda s: s.strip().lstrip("|"),
+        # interesting_fields=None
+        interesting_fields={
+            "Logic Cells",
+            "LUT-Only LCs",
+            "Register-Only LCs",
+            "LUT/Register LCs",
+            "Dedicated Logic Registers",
+            "ALMs needed [=A-B+C]",
+            "Combinational ALUTs",
+            "ALMs used for memory",
+            "Memory Bits",
+            "M10Ks",
+            "M9Ks",
+            "DSP Elements",
+            "DSP Blocks",
+            "Block Memory Bits",
+            "Pins",
+            "I/O Registers",
+        }
+        # ['Logic Cells', 'Memory Bits', 'M10Ks', 'M9Ks', 'DSP Elements', 'ALMs needed [=A-B+C]',
+        #                     'Combinational ALUTs', 'ALMs used for memory', 'DSP Blocks', 'Pins'
+        #                     'LUT-Only LCs',	'Register-Only LCs', 'LUT/Register LCs', 'Block Memory Bits']
+    )
+    assert resources == {
+        "full_adder_piped": {
+            "ALMs needed [=A-B+C]": 1.5,
+            "ALMs used for memory": 0.0,
+            "Block Memory Bits": 0,
+            "Combinational ALUTs": 3,
+            # 'Compilation Hierarchy Node': '|full_adder_piped',
+            "DSP Blocks": 0,
+            "Dedicated Logic Registers": 2,
+            # 'Entity Name': 'full_adder_piped',
+            # 'Full Hierarchy Name': '|full_adder_piped',
+            "I/O Registers": 0,
+            # 'Library Name': 'work',
+            "M10Ks": 0,
+            "Pins": 7,
+            # 'Virtual Pins': 0,
+            # '[A] ALMs used in final placement': 1.5,
+            # '[B] Estimate of ALMs recoverable by dense packing': 0.0,
+            # '[C] Estimate of ALMs unavailable': 0.0
+        },
+    }
+
+
+def _test_parse_reports():
+    root_dir = Path("/Users/kamyar/src/xeda/examples/vhdl/pipeline")
+    design = Design.from_toml(root_dir / "pipelined_adder.toml")
+    run_path = root_dir / "xeda_run/pipelined_adder/quartus"
+    settings = Quartus.Settings(fpga={"part": "10CL016YU256C6G"}, clock_period=15)
+    flow = Quartus(settings, design, run_path=run_path)
+    flow.init()
+    flow.parse_reports()
+    print(flow.results)
+
+
+def test_parse_csv_no_header():
+    parsed = parse_csv(RESOURCES_DIR / "Flow_Summary.csv", None)
+    expected = {
+        "Flow Status": "Successful - Tue Mar  1 11:10:35 2022",
+        "Quartus Prime Version": "21.1.0 Build 842 10/21/2021 SJ Lite Edition",
+        "Revision Name": "pipelined_adder",
+        "Top-level Entity Name": "full_adder_piped",
+        "Family": "Cyclone V",
+        "Device": "5CGXBC3B6F23C7",
+        "Timing Models": "Final",
+        "Total registers": "2",
+        "Total pins": "7 / 222 ( 3 % )",
+        "Total virtual pins": "0",
+        "Total DSP Blocks": "0 / 57 ( 0 % )",
+        "Total HSSI RX PCSs": "0 / 3 ( 0 % )",
+        "Total HSSI PMA RX Deserializers": "0 / 3 ( 0 % )",
+        "Total HSSI TX PCSs": "0 / 3 ( 0 % )",
+        "Total HSSI PMA TX Serializers": "0 / 3 ( 0 % )",
+        "Total PLLs": "0 / 7 ( 0 % )",
+        "Total DLLs": "0 / 3 ( 0 % )",
+    }
+
+    for k, v in expected.items():
+        assert parsed[k] == v
+
+
+def prepend_to_path(path):
+    current_path = os.environ.get("PATH", "").split(os.pathsep)
+    current_path.insert(0, str(path))
+    os.environ["PATH"] = os.pathsep.join(current_path)
+
+
+def test_quartus_synth_py() -> None:
+    path = RESOURCES_DIR / "design0/design0.toml"
+    # Append to PATH so if the actual tool exists, would take precedences.
+    dockerized = True
+    if "PYTEST_CURRENT_TEST" in os.environ:
+        prepend_to_path(TESTS_DIR / "fake_tools")
+        dockerized = False
+    assert path.exists()
+    design = Design.from_toml(EXAMPLES_DIR / "vhdl" / "sqrt" / "sqrt.toml")
+    settings = dict(fpga=FPGA("10CL016YU256C6G"), clock_period=6, dockerized=dockerized)
+    with tempfile.TemporaryDirectory() as run_dir:
+        print("Xeda run dir: ", run_dir)
+        xeda_runner = DefaultRunner(run_dir, debug=True)
+        flow = xeda_runner.run_flow(Quartus, design, settings)
+        settings_json = flow.run_path / "settings.json"
+        results_json = flow.run_path / "results.json"
+        assert settings_json.exists()
+        assert results_json.exists()
+        assert flow.succeeded
+
+
+if __name__ == "__main__":
+    _test_parse_reports()
+    # test_quartus_synth_py()
diff --git a/tests/test_vivado.py b/tests/test_vivado.py
new file mode 100644
index 00000000..5f01b6f8
--- /dev/null
+++ b/tests/test_vivado.py
@@ -0,0 +1,58 @@
+import os
+import tempfile
+from pathlib import Path
+
+from xeda import Design
+from xeda.flow_runner import DefaultRunner
+from xeda.flows import VivadoSynth
+from xeda.flows.flow import FPGA
+
+TESTS_DIR = Path(__file__).parent.absolute()
+RESOURCES_DIR = TESTS_DIR / "resources"
+EXAMPLES_DIR = TESTS_DIR.parent / "examples"
+
+
+def test_vivado_synth_template() -> None:
+    path = RESOURCES_DIR / "design0/design0.toml"
+    assert path.exists()
+    design = Design.from_toml(RESOURCES_DIR / "design0/design0.toml")
+    settings = VivadoSynth.Settings(fpga=FPGA(part="abcd"), clock_period=5.5)  # type: ignore
+    run_dir = Path.cwd() / "vivado_synth_run"
+    run_dir.mkdir(exist_ok=True)
+    flow = VivadoSynth(settings, design, run_dir)  # type: ignore
+    tcl_file = flow.copy_from_template(
+        "vivado_synth.tcl", xdc_files=[], reports_tcl="reports_tcl"
+    )
+    with open(run_dir / tcl_file) as f:
+        vivado_tcl = f.read()
+    expected_lines = [
+        "set_property generic {G_IN_WIDTH=32} [current_fileset]",
+        "set_property generic {G_ITERATIVE=1'b1} [current_fileset]",
+        'set_property generic {G_STR=\\"abcd\\"} [current_fileset]',
+        "set_property generic {G_BITVECTOR=7'b0101001} [current_fileset]",
+    ]
+    for line in expected_lines:
+        assert line in vivado_tcl
+
+
+def test_vivado_synth_py() -> None:
+    path = RESOURCES_DIR / "design0/design0.toml"
+    # Append to PATH so if the actual tool exists, would take precedences.
+    os.environ["PATH"] += os.pathsep + os.path.join(TESTS_DIR, "fake_tools")
+    assert path.exists()
+    design = Design.from_toml(EXAMPLES_DIR / "vhdl" / "sqrt" / "sqrt.toml")
+    settings = dict(fpga=FPGA("xc7a12tcsg325-1"), clock_period=5.5)
+    with tempfile.TemporaryDirectory() as run_dir:
+        print("Xeda run dir: ", run_dir)
+        xeda_runner = DefaultRunner(run_dir, debug=True)
+        flow = xeda_runner.run_flow(VivadoSynth, design, settings)
+        settings_json = flow.run_path / "settings.json"
+        results_json = flow.run_path / "results.json"
+        assert settings_json.exists()
+        assert results_json.exists()
+        assert flow.succeeded
+        assert 0.3 < flow.results.runtime  # type: ignore
+
+
+if __name__ == "__main__":
+    test_vivado_synth_py()
diff --git a/tests/test_yosys.py b/tests/test_yosys.py
new file mode 100644
index 00000000..e69de29b