The Nuskell programming language¶
Translation schemes are design algorithms that translate a chemical
reaction network (CRN) into a domain-level strand displacement (DSD) system.
The Nuskell programming language is inspired by the functional programming
language Haskell and provides DSD specific classes, functions and macros to
generalize translations for arbitrary CRNs.
This section describes the syntax of the Nuskell programming language in
order to add new translation schemes to the scheme library.
A library of existing schemes can be found in the official Nuskell repository.
Syntax¶
Every translation scheme consists of variable assignments in the form of:
name = value
and functions in the form of:
declarator function(arg, ...) = value ;
The where keyword allows for more verbose formulations, the character #
is used for (inline) comments:
# This is a comment.
declarator function(arg, ...) = result
where
result = value; # delimiter ';' closes the function
declarator function1(arg, ...) = result1 + result2
where {
result1 = value; # delimiter ';' separates two assignments
result2 = function2(value) # no delimiter ';' before closing '}'
};
Conditional statements can be written using if, then, else,
elseif keywords. The operators and, or, *, /, +,
-, ==, !=, <, >, >=, <= are supported and logically
equivalent to their implementation in Python:
# Operators are treated as in Python.
declarator function1(arg1, arg2, arg3, arg4) = result
where
result =
if arg1 < arg2 then
value1
elseif arg3 and arg4 then
value2
else
value3 ;
Note that white space formatting is optional, all statements above can be written
on a single line. However, the use of {} and ; as delimiters is obligatory.
Every function assignment has to be closed by a ;. A where statement can
be followed by a single assignment, or a list of assignments {} with a ; as
delimiter.
Built-in functions¶
The Nuskell language provides a number of built-in functions.
y = short()– returns a new toehold domain.y = long()– returns a new branch-migration domain.y = infty(x)– returns a set with one fuel from input complexx(assignsinfiniteconcentration)y = empty– return an empty set of fuels.y = tail(x)– return a listxwithout its first element.y = flip(x)– return a transposed matrixx. Similar to Python’s zip().y = rev_reactions(crn)– return inputcrnsuch that corresponding irreversible reactions are combined to reversible reactionsy = irrev_reactions(crn)– return inputcrnsuch that reversible reactions are split into two irreversible reactionsy = print(m)– print messagem, return nothing(!)y = abort(m)– exit with messagem, return nothing(!)y = len(x)– returns the length of listxy = sum(x)– return the sum of listx. Often used to sum over sets of fuels.y = range(x)– returns a list[0 .. x-1]y = reverse(x)– returns listxin reversey = map(f,x)– applies functionf(x)to every element in listxy = map2(f,y, x)– applies functionf(y,x)to every element in listxy = birxn(x)–y = unirxn(x)–y = rxn_degree(x,r)–
Note that even though the functions print(m) and abort(m) do not have a
return value, the Nuskell language syntax of requires them to be formulated
within an assignment. For example:
# print() and abort() have no return value.
declarator function1(arg1, arg2) = result
where {
void = print('Computing result:');
result =
if arg1 < arg2 then
abort('Error:', arg1, '<', arg2)
else
arg2 - arg1;
void = print('Returning value:', result)
};
Function declarators¶
Translation schemes can use a variety of function declarators to indicate function return values:
function– a recursive definition of a function. As an example, some of the built-in functions described above are implemented within the Nuskell language:function len(x) = if x == [] then 0 else 1 + len(tail(x)) ; # tail(x) returns list x without the first element. function sum(x) = if len(x) == 0 then empty elseif len(x) == 1 then x[0] else x[0] + sum(tail(x)) ; function map(f, x) = if len(x) == 0 then [] else [f(x[0])] + map(f, tail(x)) ;class– returns a domain-level complex or a list of domain-level complexes. Domain-level complexes are specified as a tuple of sequence and structure, for example:"a b a*" | "( . )"denotes a single strand with three domains forming a hairpin loop. By conventiona*denotes a domain complementary toa. On the other hand,["a b a*" | ". . .", "a b a*" | "( . )"]is a list of two molecules, which differ in their secondary structure, but not in their sequence.class get_complexes() = ["a b a*" | "( . )", "a b a*" | ". . ."] where { a = short(); # Note that "a*" is implicitly assigned, as the complement of "a" b = long() }; # The required class "formal(s)" must read one argument (a formal species) # and returns a single domain-level complex. The wildcard "?" can be used to # specify history domains, enabling a many to one mapping from singal to # formal species. class formal(s) = "? t f" | ". . ." where { t = short(); f = long() }; # In most cases, a class will translate a list of reactant signal species "r" # and product signal species "p" into a domain-level complex: class binary_fuel_complexes(r, p) = [ "a t i + b t k + ch t c + dh t d + t* dh* t* ch* t* b* t* a* t*" | "( ( . + ( ( . + ( ( . + ( ( . + ) ) ) ) ) ) ) ) . ", "a t i" | " . . . ", "t ch t dh t" | ". . . . ." ] where { a = r[0].f; # The domain f of the first signal species in list r b = r[1].f; c = p[0].f; ch = long(); d = p[1].f; dh = long(); i = long(); k = long(); t = short() };macro– has the same return value asclass, but is used to denote partial domain-level complexes, i.e. aclasscan employmacrosto generalize translation schemes on the domain-level for arbitrary CRNs.# TODO class get_fuels() = [] where { [l, p, q] = flip(map(chen2D_O, reverse(prod)), 2); [l, p, q] = zip(map(chen2D_O, reverse(prod)));module– returns a set() of fuel complexes. Fuel complexes are domain-level complexes with, ideally, constant concentration. Nuskell uses the keywordinftyto translate a domain-level complex into a fuel complex. The example code below starts with themodule main()which takes the input CRN as argument. We will discuss the properties of thecrnobject in detail later (see The crn object).# The *required* module "main(crn)" translates a CRN into a set of fuel species. module main(crn) = sum(map(get_fuels, crn)) where crn = irrev_reactions(crn); module get_fuels(reaction) = sum(map(infty, complexes)) where complexes = get_complexes(reaction.reactants, reaction.products) ;global– returns a global variable, such as a global domain.global toehold = short() ;
Note: Nuskell does not enforce the proper usage of class, function,
module and macro, they can be used interchangeably. Only, the
global declarator is specific to the use of global variables.
The crn object¶
Tutorial script 1 - Fist Steps¶
There are two required parts: (i) the formal class defines sequence and
structure of signal complexes, (ii) the main module produces a set of fuel
species from the input CRN. The following translation scheme translates CRNs in
binary format (two reactants, two products) into a DSD system, and aborts the
translation if it encounters a non-binary reaction.
The classes define signal and fuel complexes, the modules contain instructions
to design fuel complexes specific for a reaction of formal species.
# -----------------------------------------------------------------------------
# Translate formal reactions with two reactants and two products.
# Lakin et. al (2012) "Abstractions for DNA circuit design." [Figure 5]
# -----------------------------------------------------------------------------
#
# Coded by Stefan Badelt (badelt@caltech.edu)
# Define a global short toehold domain
global toehold = short();
# Write a class to define domains and structure of signal species
# ? is a wildcard for a history domain.
class formal(s) = "? t f" | ". . ."
where { t = toehold ; f = long() };
# Write a class to produce fuel complexes for bimolecular reactions
class bimol_fuels(r, p) =
[ "a t i + b t k + ch t c + dh t d + t* dh* t* ch* t* b* t* a* t*"
| "( ( . + ( ( . + ( ( . + ( ( . + ) ) ) ) ) ) ) ) . ",
"a t i" | " . . . ", "t ch t dh t" | ". . . . ." ]
where {
a = r[0].f;
b = r[1].f;
c = p[0].f; ch = long();
d = p[1].f; dh = long();
i = long(); k = long();
t = toehold };
# Write a module that applies the fuel production to every reaction
module rxn(r) = sum(map(infty, fuels))
where fuels =
if len(r.reactants) != 2 or len(r.products) != 2 then
abort('Reaction type not implemented')
else
bimol_fuels(r.reactants, r.products);
# Write the module *main* that applies *rxn* to the crn.
module main(crn) = sum(map(rxn, crn))
where crn = irrev_reactions(crn);
Tutorial script 2 - Generalization¶
Tutorial script 3 - Optimization¶
Built-In Functions¶
Built-in functions written in the Nuskell programming language:
function range(x) = if x == 0 then [] else range(x - 1) + [x - 1] ; function sum(x) = if len(x) == 0 then empty elseif len(x) == 1 then x[0] else x[0] + sum(tail(x)) ; function len(x) = if x == [] then 0 else 1 + len(tail(x)) ; function reverse(x) = if x == [] then [] else reverse(tail(x)) + [x[0]] ; function rxn_degree(x, r) = if len(x) == 0 then [] elseif len(x[0].reactants) == r then [x[0]] + rxn_degree(tail(x), r) else rxn_degree(tail(x), r) ; function unirxn(x) = if len(x) == 0 then [] elseif len(x[0].reactants) == 1 then [x[0]] + unirxn(tail(x)) else unirxn(tail(x)) ; function birxn(x) = if len(x) == 0 then [] elseif len(x[0].reactants) == 2 then [x[0]] + birxn(tail(x)) else birxn(tail(x)) ; function map(f, x) = if len(x) == 0 then [] else [f(x[0])] + map(f, tail(x)) ; function map2(f, y, x) = if len(x) == 0 then [] else [f(y, x[0])] + map2(f, y, tail(x))