Python API

Requires Odatix 4.0+

Every file documented in this section can be written by hand, and that is often the shortest path. When it is not — a workspace generated from a spreadsheet, a CI job that adds a target, a script that sweeps a design over several technologies — the same files are read and written by the odatix.workspace API, and by the odatix config command built on it. What they describe is then run through odatix.run.

These are the very APIs Odatix itself uses: what a page of the graphical interface does to a workspace, and what a command runs, is exactly what a script does through them.

Opening a workspace

Everything hangs off a Workspace. Paths are resolved once, from odatix.yml, so nothing else takes a path.

open.py
from odatix.workspace import Workspace

ws = Workspace.open()              # the workspace of the current directory
ws = Workspace.open("~/designs")   # or another one
ws = Workspace.init("new_dir")     # create the configuration files, then open it

print(ws.paths.arch_path)          # where its architectures are
print(ws.architectures.names())    # what it holds

Workspace.open() never fails on a directory that holds no odatix.yml: the Odatix defaults apply, which is what a workspace about to be initialized looks like. Pass required=True to refuse it instead.

What a workspace holds

Attribute What it gives
ws.architectures the architectures
ws.simulations the simulations
ws.workflows the workflows
ws.tools the eda tools, built-in ones included
ws.targets the target files, one per tool
ws.jobs the run settings files, one per command
ws.derived_metrics the derived metrics of the workspace
ws.paths where each of them is

Collections behave like the mappings they are, and iterate over objects:

browse.py
"MyCPU" in ws.architectures        # True / False
ws.architectures["MyCPU"]          # raises NotFoundError if there is none
ws.architectures.get("MyCPU")      # None instead
ws.architectures.entry("MyCPU")    # whether or not it exists yet

for architecture in ws.architectures:
    print(f"{architecture.name} => {architecture.settings.top_level_module}")

Settings objects

Settings are typed: they know their keys, their defaults and how each one is written. They are also mappings, so code that would rather use them as such can.

settings.py
architecture = ws.architectures.create("MyCPU")

architecture.settings.rtl_path = "rtl/cpu"
architecture.settings.top_level_file = "cpu.sv"
architecture.settings.top_level_module = "cpu"
architecture.settings.clock_signal = "clk"
architecture.settings.use_parameters = True
architecture.settings.fmax_synthesis.lower_bound = 50
architecture.save()

architecture.update(top_level_module="cpu_top")   # change and save, in one call

architecture.settings["clock_signal"]             # same thing, as a mapping
architecture.settings.to_dict()                   # plain values

Values are read as their type whatever they come as, so "Yes", true and True all mean the same thing, and "50" is stored as 50.

Note

Saving keeps what you put in the file: its comments, its key order, its quoting and every key Odatix does not know about. Only what actually changed is rewritten, and unknown keys stay reachable through settings.extra. A file that does not exist yet is generated with the section comments that make it readable.

Parameter domains and configurations

An architecture (or a workflow) carries a main parameter domain and any number of named ones. Both behave the same here.

domains.py
architecture.configs.write("08bits", "\n  parameter WIDTH = 8;\n")
architecture.configs.names()                 # ["08bits"], no ".txt"
architecture.configs["08bits"].read()

width = architecture.domains.create("width", param_target_file="rtl/cpu.sv")
width.settings.start_delimiter = "#("
width.settings.stop_delimiter = ")"
width.save()

architecture.combinations()                  # what a run would sweep
architecture.count_combinations()

Configurations can be generated instead of written, from the same settings odatix generate reads:

generate.py
generation = width.settings.generate_configurations_settings
generation.name = "${width}bits"
generation.template = "WIDTH = ${width}"
width.settings.set_variable("width", "range", {"from": 8, "to": 64, "step": 8})
width.settings.generate_configurations = True
width.save()

width.preview_configurations()               # {name: content}, writes nothing
width.generate_configurations(overwrite=True)

Tools and targets

tools.py
tool = ws.tools["vivado"]
tool.is_builtin                              # shipped with Odatix
tool.settings.flow_names()                   # ["standard", "power_opt"]
tool.settings.default_flow.command("fmax_synthesis")

tool.settings.label = "Vivado 2024.1"
tool.save()          # writes only what differs from the built-in definition

targets = ws.targets["vivado"]
targets.enabled_names()                      # what runs
targets.add("xc7a100t-csg324-1")
targets.disable("xc7s25-csga225-1")          # kept in the file, commented out

A tool of your own is created the same way, and is written whole:

own_tool.py
tool = ws.tools.create("my_tool", label="My Tool")
tool.settings.default_flow.set_command("fmax_synthesis", ["make fmax"])
tool.save()

What each command runs

ws.jobs holds one entry per run command, each with the settings of its file.

jobs.py
run = ws.jobs.fmax_synthesis
run.settings.architectures = ["MyCPU/08bits", "MyCPU/16bits"]
run.settings.nb_jobs = "auto"
run.settings.fmax_synthesis.lower_bound = 50
run.save()

ws.jobs.simulation.settings.simulations = {"TB_Counter": ["MyCPU/08bits"]}
ws.jobs.simulation.save()

ws.jobs["analysis"].settings.tools = ["vivado", "verilator"]

Reading one the way a run needs it — every required key spelled out, values of the right kind — is load(), which raises InvalidSettingsError on a file a run could not start from:

load.py
settings = ws.jobs.fmax_synthesis.load()
print(settings.nb_jobs, settings.architectures)

Running

odatix.run starts what those files describe. A run goes through three steps, and stopping after any of them is a normal thing to do.

run.py
from odatix.workspace import Workspace
from odatix.run import Run

run = Run(Workspace.open(), "fmax_synthesis", tool="vivado", overwrite=True)

plan = run.check()      # what would be run, having touched nothing
print(plan.counts())    # {'new': 12, 'cached': 3, 'error': 0, ...}

run.prepare()           # every work directory written, nothing started
run.start()             # handed over to the daemon

Each step does the ones before it when they have not been done, so run.start() alone runs everything, and run_job("fmax_synthesis", tool="vivado") is the whole thing in one call.

Every path comes from the workspace. What a run does differently from what its settings file says is passed as keyword arguments — the command line flags, by name: overwrite, nb_jobs, flow, until, keep, lower_bound / upper_bound, frequencies, detach, session…

Mode What it runs
"fmax_synthesis" the fmax binary search of odatix fmax
"custom_freq_synthesis" the synthesis at given frequencies of odatix synth
"pnr" the place & route of odatix pnr
"analysis" the RTL analysis of odatix analyze
"simulation" the simulations of odatix sim
"workflow" the workflows of odatix workflow

A run never stops the interpreter and never asks a question: what it cannot do raises RunError, carrying what it reported, and everything it said along the way is on run.reporter.

errors.py
from odatix.run import RunError

try:
    run.check()
except RunError as error:
    print(error)              # what went wrong
    print(error.errors())     # everything reported as an error

Errors

Exception Raised when
NotFoundError there is no such architecture, domain, tool, target… (also a KeyError)
AlreadyExistsError the name asked for is taken (also a ValueError)
InvalidNameError the name cannot be a directory name (empty, contains /…)
NotAWorkspaceError the directory holds no settings file, and one was required
InvalidSettingsError a settings file cannot be run from (missing, unreadable, incomplete)

All of them derive from WorkspaceError. A run raises RunError (it could not go on) and RunCancelled (it was asked to stop).

See also