diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml
index e6b5e1f..d43955e 100644
--- a/.JuliaFormatter.toml
+++ b/.JuliaFormatter.toml
@@ -1,5 +1,5 @@
 indent = 4
-margin = 80
+margin = 120
 always_for_in = true
 for_in_replacement = "in"
 whitespace_typedefs = true
diff --git a/docs/src/lib/internals/models.md b/docs/src/lib/internals/models.md
index 192b91c..a2537e3 100644
--- a/docs/src/lib/internals/models.md
+++ b/docs/src/lib/internals/models.md
@@ -1,5 +1,15 @@
 # Models
 
+## Interface
+
+The interface that has to be implemented for a model to be usable is defined in `src/models/interface.jl`.
+
+```@autodocs
+Modules = [MetagraphOptimization]
+Pages = ["models/interface.jl"]
+Order = [:type, :constant, :function]
+```
+
 ## ABC-Model
 
 ### Types
diff --git a/examples/import_bench.jl b/examples/import_bench.jl
index 64a5000..5143504 100644
--- a/examples/import_bench.jl
+++ b/examples/import_bench.jl
@@ -13,16 +13,15 @@ function bench_txt(filepath::String, bench::Bool = true)
         return
     end
 
+    model = ABCModel()
+
     println(name, ":")
-    g = parse_abc(filepath)
+    g = parse_dag(filepath, model)
     print(g)
-    println(
-        "  Graph size in memory: ",
-        bytes_to_human_readable(MetagraphOptimization.mem(g)),
-    )
+    println("  Graph size in memory: ", bytes_to_human_readable(MetagraphOptimization.mem(g)))
 
     if (bench)
-        @btime parse_abc($filepath)
+        @btime parse_dag($filepath, $model)
     end
 
     println("  Get Operations: ")
diff --git a/examples/plot_chain.jl b/examples/plot_chain.jl
index 4e9cb1c..3b57044 100644
--- a/examples/plot_chain.jl
+++ b/examples/plot_chain.jl
@@ -12,7 +12,7 @@ function gen_plot(filepath)
         return
     end
 
-    g = parse_abc(filepath)
+    g = parse_dag(filepath, ABCModel())
 
     Random.seed!(1)
 
@@ -48,23 +48,10 @@ function gen_plot(filepath)
 
     println("\rDone.")
 
-    plot(
-        [x[1], x[2]],
-        [y[1], y[2]],
-        linestyle = :solid,
-        linewidth = 1,
-        color = :red,
-        legend = false,
-    )
+    plot([x[1], x[2]], [y[1], y[2]], linestyle = :solid, linewidth = 1, color = :red, legend = false)
     # Create lines connecting the reference point to each data point
     for i in 3:length(x)
-        plot!(
-            [x[i - 1], x[i]],
-            [y[i - 1], y[i]],
-            linestyle = :solid,
-            linewidth = 1,
-            color = :red,
-        )
+        plot!([x[i - 1], x[i]], [y[i - 1], y[i]], linestyle = :solid, linewidth = 1, color = :red)
     end
 
     return gui()
diff --git a/examples/plot_star.jl b/examples/plot_star.jl
index 3f82fb5..8f8ad68 100644
--- a/examples/plot_star.jl
+++ b/examples/plot_star.jl
@@ -12,7 +12,7 @@ function gen_plot(filepath)
         return
     end
 
-    g = parse_abc(filepath)
+    g = parse_dag(filepath, ABCModel())
 
     Random.seed!(1)
 
@@ -60,14 +60,7 @@ function gen_plot(filepath)
         push!(y, props.computeEffort)
         pop_operation!(g)
 
-        push!(
-            names,
-            "NF: (" *
-            string(props.data) *
-            ", " *
-            string(props.computeEffort) *
-            ")",
-        )
+        push!(names, "NF: (" * string(props.data) * ", " * string(props.computeEffort) * ")")
     end
     for op in opt.nodeReductions
         push_operation!(g, op)
@@ -76,14 +69,7 @@ function gen_plot(filepath)
         push!(y, props.computeEffort)
         pop_operation!(g)
 
-        push!(
-            names,
-            "NR: (" *
-            string(props.data) *
-            ", " *
-            string(props.computeEffort) *
-            ")",
-        )
+        push!(names, "NR: (" * string(props.data) * ", " * string(props.computeEffort) * ")")
     end
     for op in opt.nodeSplits
         push_operation!(g, op)
@@ -92,33 +78,13 @@ function gen_plot(filepath)
         push!(y, props.computeEffort)
         pop_operation!(g)
 
-        push!(
-            names,
-            "NS: (" *
-            string(props.data) *
-            ", " *
-            string(props.computeEffort) *
-            ")",
-        )
+        push!(names, "NS: (" * string(props.data) * ", " * string(props.computeEffort) * ")")
     end
 
-    plot(
-        [x0, x[1]],
-        [y0, y[1]],
-        linestyle = :solid,
-        linewidth = 1,
-        color = :red,
-        legend = false,
-    )
+    plot([x0, x[1]], [y0, y[1]], linestyle = :solid, linewidth = 1, color = :red, legend = false)
     # Create lines connecting the reference point to each data point
     for i in 2:length(x)
-        plot!(
-            [x0, x[i]],
-            [y0, y[i]],
-            linestyle = :solid,
-            linewidth = 1,
-            color = :red,
-        )
+        plot!([x0, x[i]], [y0, y[i]], linestyle = :solid, linewidth = 1, color = :red)
     end
     #scatter!(x, y, label=names)
 
diff --git a/scripts/bench_threads.fish b/scripts/bench_threads.fish
index 28df8c3..5e1ce95 100755
--- a/scripts/bench_threads.fish
+++ b/scripts/bench_threads.fish
@@ -6,20 +6,20 @@ julia --project=./examples -t 4 -e 'import Pkg; Pkg.instantiate()'
 
 #for i in $(seq $minthreads $maxthreads)
 #   printf "(AB->AB, $i) "
-#   julia --project=./examples -t $i -O3 -e 'using MetagraphOptimization; using BenchmarkTools; @btime get_operations(graph) setup=(graph = parse_abc("input/AB->AB.txt"))'
+#   julia --project=./examples -t $i -O3 -e 'using MetagraphOptimization; using BenchmarkTools; @btime get_operations(graph) setup=(graph = parse_dag("input/AB->AB.txt"), ABCModel())'
 #end
 
 #for i in $(seq $minthreads $maxthreads)
 #   printf "(AB->ABBB, $i) "
-#   julia --project=./examples -t $i -O3 -e 'using MetagraphOptimization; using BenchmarkTools; @btime get_operations(graph) setup=(graph = parse_abc("input/AB->ABBB.txt"))'
+#   julia --project=./examples -t $i -O3 -e 'using MetagraphOptimization; using BenchmarkTools; @btime get_operations(graph) setup=(graph = parse_dag("input/AB->ABBB.txt"), ABCModel())'
 #end
 
 #for i in $(seq $minthreads $maxthreads)
 #   printf "(AB->ABBBBB, $i) "
-#   julia --project=./examples -t $i -O3 -e 'using MetagraphOptimization; using BenchmarkTools; @btime get_operations(graph) setup=(graph = parse_abc("input/AB->ABBBBB.txt"))'
+#   julia --project=./examples -t $i -O3 -e 'using MetagraphOptimization; using BenchmarkTools; @btime get_operations(graph) setup=(graph = parse_dag("input/AB->ABBBBB.txt"), ABCModel())'
 #end
 
 for i in $(seq $minthreads $maxthreads)
    printf "(AB->ABBBBBBB, $i) "
-   julia --project=./examples -t $i -O3 -e 'using MetagraphOptimization; using BenchmarkTools; @btime get_operations(graph) setup=(graph = parse_abc("input/AB->ABBBBBBB.txt"))'
+   julia --project=./examples -t $i -O3 -e 'using MetagraphOptimization; using BenchmarkTools; @btime get_operations(graph) setup=(graph = parse_dag("input/AB->ABBBBBBB.txt"), ABCModel())'
 end
diff --git a/src/MetagraphOptimization.jl b/src/MetagraphOptimization.jl
index 093536a..29b595d 100644
--- a/src/MetagraphOptimization.jl
+++ b/src/MetagraphOptimization.jl
@@ -42,7 +42,6 @@ export can_pop
 export reset_graph!
 export get_operations
 
-export parse_abc
 export ComputeTaskP
 export ComputeTaskS1
 export ComputeTaskS2
@@ -51,9 +50,12 @@ export ComputeTaskU
 export ComputeTaskSum
 
 export execute
-export gen_particles
+export parse_dag, parse_process
+export gen_process_input
+export get_compute_function
 export ParticleValue
-export Particle
+export ParticleA, ParticleB, ParticleC
+export ABCProcessDescription, ABCProcessInput, ABCModel
 
 export ==, in, show, isempty, delete!, length
 
@@ -114,6 +116,8 @@ include("task/compare.jl")
 include("task/print.jl")
 include("task/properties.jl")
 
+include("models/interface.jl")
+
 include("models/abc/types.jl")
 include("models/abc/particle.jl")
 include("models/abc/compute.jl")
diff --git a/src/code_gen/main.jl b/src/code_gen/main.jl
index 56d9734..1aa4cb7 100644
--- a/src/code_gen/main.jl
+++ b/src/code_gen/main.jl
@@ -3,7 +3,7 @@ using DataStructures
 """
     gen_code(graph::DAG)
 
-Generate the code for a given graph. The return value is a tuple of:
+Generate the code for a given graph. The return value is a named tuple of:
 
 - `code::Expr`: The julia expression containing the code for the whole graph.
 - `inputSymbols::Dict{String, Vector{Symbol}}`: A dictionary of symbols mapping the names of the input nodes of the graph to the symbols their inputs should be provided on.
@@ -47,85 +47,55 @@ function gen_code(graph::DAG)
     # node is now the last node we looked at -> the output node
     outSym = Symbol("data_$(to_var_name(node.id))")
 
-    return (
-        code = Expr(:block, code...),
-        inputSymbols = inputSyms,
-        outputSymbol = outSym,
-    )
+    return (code = Expr(:block, code...), inputSymbols = inputSyms, outputSymbol = outSym)
 end
 
 function gen_input_assignment_code(
     inputSymbols::Dict{String, Vector{Symbol}},
-    inOutCount::Dict{ParticleType, Tuple{Int, Int}},
-    functionInputSymbol::Symbol = :input,
+    processDescription::AbstractProcessDescription,
+    processInputSymbol::Symbol = :input,
 )
-    @assert !isempty(particles[1]) "Can't have 0 input particles!"
-    @assert !isempty(particles[2]) "Can't have 0 output particles!"
-    @assert length(inputSymbols) >= length(particles[1]) + length(particles[2])
-
-    # TODO none of this is very pretty
-    in_out_count = Dict{ParticleType, Tuple{Int, Int}}()
-    for type in types(particles[1][1])
-        in_out_count[type] = (0, 0)
-    end
-
-    # we assume that the particles with lower numbers are the inputs, and then the output particles follow
-    for p in particles[1]
-        (i, o) = in_out_count[p.type]
-        in_out_count[p.type] = (i + 1, o)
-    end
-    for p in particles[2]
-        (i, o) = in_out_count[p.type]
-        in_out_count[p.type] = (i, o + 1)
-    end
+    @assert length(inputSymbols) >=
+            sum(values(in_particles(processDescription))) + sum(values(out_particles(processDescription))) "Number of input Symbols is smaller than the number of particles in the process description"
 
     assignInputs = Vector{Expr}()
     for (name, symbols) in inputSymbols
-        type = nothing
-        if startswith(name, "A")
-            type = A
-        elseif startswith(name, "B")
-            type = B
-        else
-            type = C
-        end
+        type = type_from_name(name)
         index = parse(Int, name[2:end])
 
         p = nothing
 
-        if (index > in_out_count[type][1])
-            index -= in_out_count[type][1]
-            @assert index <= in_out_count[type][2] "Too few particles of type $type in input particles for this process"
+        if (index > in_particles(processDescription)[type])
+            index -= in_particles(processDescription)[type]
+            @assert index <= out_particles(processDescription)[type] "Too few particles of type $type in input particles for this process"
 
-            p = "$(functionInputSymbol)[2][$(index)]"
+            p = "filter(x -> typeof(x) <: $type, out_particles($(processInputSymbol)))[$(index)]"
         else
-            p = "$(functionInputSymbol)[1][$(index)]"
+            p = "filter(x -> typeof(x) <: $type, in_particles($(processInputSymbol)))[$(index)]"
         end
 
         for symbol in symbols
-            push!(
-                assignInputs,
-                Meta.parse("$(symbol) = ParticleValue($p, 1.0)"),
-            )
+            push!(assignInputs, Meta.parse("$(symbol) = ParticleValue($p, 1.0)"))
         end
     end
 
     return Expr(:block, assignInputs...)
 end
 
+"""
+    get_compute_function(graph::DAG, process::AbstractProcessDescription)
 
-function get_compute_function(
-    graph::DAG,
-    input::Tuple{Vector{Particle}, Vector{Particle}},
-)
+Return a function of signature `compute_<id>(input::AbstractProcessInput)`, which will return the result of the DAG computation on the given input.
+"""
+function get_compute_function(graph::DAG, process::AbstractProcessDescription)
     (code, inputSymbols, outputSymbol) = gen_code(graph)
 
-    assignInputs = gen_input_assignment_code(inputSymbols, input, :input)
+    assignInputs = gen_input_assignment_code(inputSymbols, process, :input)
 
     function_id = to_var_name(UUIDs.uuid1(rng[1]))
     func = eval(
         Meta.parse(
-            "function compute_$(function_id)(input::Tuple{Vector{Particle}, Vector{Particle}}) $assignInputs; $code; return $outputSymbol; end",
+            "function compute_$(function_id)(input::AbstractProcessInput) $assignInputs; $code; return $outputSymbol; end",
         ),
     )
 
@@ -133,23 +103,20 @@ function get_compute_function(
 end
 
 """
-    execute(graph::DAG, input::Dict{ParticleType, Vector{Particle}})
+    execute(graph::DAG, process::AbstractProcessDescription, input::AbstractProcessInput)
 
 Execute the code of the given `graph` on the given input particles.
-The input particles should be sorted correctly into the dictionary to their according [`ParticleType`](@ref)s.
 
 This is essentially shorthand for
     ```julia
-    graph = parse_abc(input_file)
-    particles = gen_particles(...)
-    compute_graph = get_compute_function(graph, particles)
+    compute_graph = get_compute_function(graph, process)
     result = compute_graph(particles)
     ```
 
-See also: [`gen_particles`](@ref)
+See also: [`parse_dag`](@ref), [`parse_process`](@ref), [`gen_process_input`](@ref)
 """
-function execute(graph::DAG, input::Tuple{Vector{Particle}, Vector{Particle}})
-    func = get_compute_function(graph, input)
+function execute(graph::DAG, process::AbstractProcessDescription, input::AbstractProcessInput)
+    func = get_compute_function(graph, process)
 
     result = 0
     try
diff --git a/src/devices/detect.jl b/src/devices/detect.jl
index 65d17ed..4b2de4f 100644
--- a/src/devices/detect.jl
+++ b/src/devices/detect.jl
@@ -26,10 +26,7 @@ function get_machine_info(verbose::Bool = Base.is_interactive())
     noDevices = length(devices)
     @assert noDevices > 0 "No devices were found, but at least one NUMA node should always be available!"
 
-    return Machine(
-        devices,
-        transferRates::Matrix{Float64}(-1, noDevices, noDevices),
-    )
+    return Machine(devices, transferRates::Matrix{Float64}(-1, noDevices, noDevices))
 end
 
 """
diff --git a/src/devices/measure.jl b/src/devices/measure.jl
index 3c90d61..d1b13e1 100644
--- a/src/devices/measure.jl
+++ b/src/devices/measure.jl
@@ -3,10 +3,7 @@
 
 Measure FLOPS, RAM, cache sizes and what other properties can be extracted for the devices in the given machine.
 """
-function measure_devices!(
-    machine::Machine;
-    verbose::Bool = Base.is_interactive(),
-)
+function measure_devices!(machine::Machine; verbose::Bool = Base.is_interactive())
     for device in machine.devices
         measure_device!(device; verbose = verbose)
     end
@@ -19,10 +16,7 @@ end
 
 Measure the transfer rates between devices in the machine.
 """
-function measure_transfer_rates!(
-    machine::Machine;
-    verbose::Bool = Base.is_interactive(),
-)
+function measure_transfer_rates!(machine::Machine; verbose::Bool = Base.is_interactive())
 
     return nothing
 end
diff --git a/src/diff/type.jl b/src/diff/type.jl
index 160df83..a12db7d 100644
--- a/src/diff/type.jl
+++ b/src/diff/type.jl
@@ -5,13 +5,7 @@ A named tuple representing a difference of added and removed nodes and edges on
 """
 const Diff = NamedTuple{
     (:addedNodes, :removedNodes, :addedEdges, :removedEdges, :updatedChildren),
-    Tuple{
-        Vector{Node},
-        Vector{Node},
-        Vector{Edge},
-        Vector{Edge},
-        Vector{Tuple{Node, String, String}},
-    },
+    Tuple{Vector{Node}, Vector{Node}, Vector{Edge}, Vector{Edge}, Vector{Tuple{Node, String, String}}},
 }
 
 function Diff()
diff --git a/src/graph/interface.jl b/src/graph/interface.jl
index 450419c..0fa74cc 100644
--- a/src/graph/interface.jl
+++ b/src/graph/interface.jl
@@ -38,8 +38,7 @@ end
 
 Return `true` if [`pop_operation!`](@ref) is possible, `false` otherwise.
 """
-can_pop(graph::DAG) =
-    !isempty(graph.operationsToApply) || !isempty(graph.appliedOperations)
+can_pop(graph::DAG) = !isempty(graph.operationsToApply) || !isempty(graph.appliedOperations)
 
 """
     reset_graph!(graph::DAG)
diff --git a/src/graph/mute.jl b/src/graph/mute.jl
index 388bdae..b5af362 100644
--- a/src/graph/mute.jl
+++ b/src/graph/mute.jl
@@ -15,12 +15,7 @@ Insert the node into the graph.
 
 See also: [`remove_node!`](@ref), [`insert_edge!`](@ref), [`remove_edge!`](@ref)
 """
-function insert_node!(
-    graph::DAG,
-    node::Node;
-    track = true,
-    invalidate_cache = true,
-)
+function insert_node!(graph::DAG, node::Node; track = true, invalidate_cache = true)
     # 1: mute
     push!(graph.nodes, node)
 
@@ -50,13 +45,7 @@ Insert the edge between node1 (child) and node2 (parent) into the graph.
 
 See also: [`insert_node!`](@ref), [`remove_node!`](@ref), [`remove_edge!`](@ref)
 """
-function insert_edge!(
-    graph::DAG,
-    node1::Node,
-    node2::Node;
-    track = true,
-    invalidate_cache = true,
-)
+function insert_edge!(graph::DAG, node1::Node, node2::Node; track = true, invalidate_cache = true)
     @assert (node2 ∉ node1.parents) && (node1 ∉ node2.children) "Edge to insert already exists"
 
     # 1: mute
@@ -95,12 +84,7 @@ Remove the node from the graph.
 
 See also: [`insert_node!`](@ref), [`insert_edge!`](@ref), [`remove_edge!`](@ref)
 """
-function remove_node!(
-    graph::DAG,
-    node::Node;
-    track = true,
-    invalidate_cache = true,
-)
+function remove_node!(graph::DAG, node::Node; track = true, invalidate_cache = true)
     @assert node in graph.nodes "Trying to remove a node that's not in the graph"
 
     # 1: mute
@@ -134,13 +118,7 @@ Remove the edge between node1 (child) and node2 (parent) into the graph.
 
 See also: [`insert_node!`](@ref), [`remove_node!`](@ref), [`insert_edge!`](@ref)
 """
-function remove_edge!(
-    graph::DAG,
-    node1::Node,
-    node2::Node;
-    track = true,
-    invalidate_cache = true,
-)
+function remove_edge!(graph::DAG, node1::Node, node2::Node; track = true, invalidate_cache = true)
     # 1: mute
     pre_length1 = length(node1.parents)
     pre_length2 = length(node2.children)
@@ -197,13 +175,7 @@ function replace_children!(task::AbstractTask, before, after)
     return nothing
 end
 
-function update_child!(
-    graph::DAG,
-    n::Node,
-    child_before::String,
-    child_after::String;
-    track = true,
-)
+function update_child!(graph::DAG, n::Node, child_before::String, child_after::String; track = true)
     # only need to update fused compute tasks
     if !(typeof(n.task) <: FusedComputeTask)
         return nothing
@@ -212,16 +184,12 @@ function update_child!(
     replace_children!(n.task, child_before, child_after)
 
     if !((child_after in n.task.t1_inputs) || (child_after in n.task.t2_inputs))
-        println(
-            "------------------ Did not replace anything!! ------------------",
-        )
+        println("------------------ Did not replace anything!! ------------------")
         child_ids = Vector{String}()
         for child in n.children
             push!(child_ids, "$(child.id)")
         end
-        println(
-            "From $(child_before) to $(child_after) in $n with children $(child_ids)",
-        )
+        println("From $(child_before) to $(child_after) in $n with children $(child_ids)")
         @assert false
     end
 
diff --git a/src/graph/print.jl b/src/graph/print.jl
index c7e66e4..5b130e7 100644
--- a/src/graph/print.jl
+++ b/src/graph/print.jl
@@ -62,9 +62,5 @@ function show(io::IO, graph::DAG)
     properties = get_properties(graph)
     println(io, "  Total Compute Effort: ", properties.computeEffort)
     println(io, "  Total Data Transfer: ", properties.data)
-    return println(
-        io,
-        "  Total Compute Intensity: ",
-        properties.computeIntensity,
-    )
+    return println(io, "  Total Compute Intensity: ", properties.computeIntensity)
 end
diff --git a/src/graph/type.jl b/src/graph/type.jl
index 64ef860..6aa8585 100644
--- a/src/graph/type.jl
+++ b/src/graph/type.jl
@@ -17,7 +17,7 @@ end
 
 The representation of the graph as a set of [`Node`](@ref)s.
 
-A DAG can be loaded using the appropriate parse function, e.g. [`parse_abc`](@ref).
+A DAG can be loaded using the appropriate parse_dag function, e.g. [`parse_dag`](@ref).
 
 [`Operation`](@ref)s can be applied on it using [`push_operation!`](@ref) and reverted using [`pop_operation!`](@ref) like a stack.
 To get the set of possible operations, use [`get_operations`](@ref).
@@ -52,11 +52,7 @@ end
 Construct and return an empty [`PossibleOperations`](@ref) object.
 """
 function PossibleOperations()
-    return PossibleOperations(
-        Set{NodeFusion}(),
-        Set{NodeReduction}(),
-        Set{NodeSplit}(),
-    )
+    return PossibleOperations(Set{NodeFusion}(), Set{NodeReduction}(), Set{NodeSplit}())
 end
 
 """
diff --git a/src/models/abc/compute.jl b/src/models/abc/compute.jl
index 62ab319..b92c03d 100644
--- a/src/models/abc/compute.jl
+++ b/src/models/abc/compute.jl
@@ -45,14 +45,12 @@ For valid inputs, both input particles should have the same momenta at this poin
 12 FLOP.
 """
 function compute(::ComputeTaskS2, data1::ParticleValue, data2::ParticleValue)
-    @assert isapprox(
-        abs(data1.p.momentum.E),
-        abs(data2.p.momentum.E),
-        rtol = 0.001,
-    ) "E: $(data1.p.momentum.E) vs. $(data2.p.momentum.E)"
-    @assert isapprox(data1.p.momentum.px, -data2.p.momentum.px, rtol = 0.001) "px: $(data1.p.momentum.px) vs. $(data2.p.momentum.px)"
-    @assert isapprox(data1.p.momentum.py, -data2.p.momentum.py, rtol = 0.001) "py: $(data1.p.momentum.py) vs. $(data2.p.momentum.py)"
-    @assert isapprox(data1.p.momentum.pz, -data2.p.momentum.pz, rtol = 0.001) "pz: $(data1.p.momentum.pz) vs. $(data2.p.momentum.pz)"
+    #=
+    @assert isapprox(abs(data1.p.momentum.E), abs(data2.p.momentum.E), rtol = 0.001, atol = sqrt(eps())) "E: $(data1.p.momentum.E) vs. $(data2.p.momentum.E)"
+    @assert isapprox(data1.p.momentum.px, -data2.p.momentum.px, rtol = 0.001, atol = sqrt(eps())) "px: $(data1.p.momentum.px) vs. $(data2.p.momentum.px)"
+    @assert isapprox(data1.p.momentum.py, -data2.p.momentum.py, rtol = 0.001, atol = sqrt(eps())) "py: $(data1.p.momentum.py) vs. $(data2.p.momentum.py)"
+    @assert isapprox(data1.p.momentum.pz, -data2.p.momentum.pz, rtol = 0.001, atol = sqrt(eps())) "pz: $(data1.p.momentum.pz) vs. $(data2.p.momentum.pz)"
+    =#
     return data1.v * inner_edge(data1.p) * data2.v
 end
 
@@ -92,11 +90,7 @@ end
 
 Generate and return code evaluating [`ComputeTaskP`](@ref) on `inExpr`, providing the output on `outExpr`.
 """
-function get_expression(
-    ::ComputeTaskP,
-    inExprs::Vector{String},
-    outExpr::String,
-)
+function get_expression(::ComputeTaskP, inExprs::Vector{String}, outExpr::String)
     return Meta.parse("$outExpr = compute(ComputeTaskP(), $(inExprs[1]))")
 end
 
@@ -106,11 +100,7 @@ end
 Generate code evaluating [`ComputeTaskU`](@ref) on `inExpr`, providing the output on `outExpr`.
 `inExpr` should be of type [`ParticleValue`](@ref), `outExpr` will be of type [`ParticleValue`](@ref).
 """
-function get_expression(
-    ::ComputeTaskU,
-    inExprs::Vector{String},
-    outExpr::String,
-)
+function get_expression(::ComputeTaskU, inExprs::Vector{String}, outExpr::String)
     return Meta.parse("$outExpr = compute(ComputeTaskU(), $(inExprs[1]))")
 end
 
@@ -120,14 +110,8 @@ end
 Generate code evaluating [`ComputeTaskV`](@ref) on `inExpr1` and `inExpr2`, providing the output on `outExpr`.
 `inExpr1` and `inExpr2` should be of type [`ParticleValue`](@ref), `outExpr` will be of type [`ParticleValue`](@ref).
 """
-function get_expression(
-    ::ComputeTaskV,
-    inExprs::Vector{String},
-    outExpr::String,
-)
-    return Meta.parse(
-        "$outExpr = compute(ComputeTaskV(), $(inExprs[1]), $(inExprs[2]))",
-    )
+function get_expression(::ComputeTaskV, inExprs::Vector{String}, outExpr::String)
+    return Meta.parse("$outExpr = compute(ComputeTaskV(), $(inExprs[1]), $(inExprs[2]))")
 end
 
 """
@@ -136,14 +120,8 @@ end
 Generate code evaluating [`ComputeTaskS2`](@ref) on `inExpr1` and `inExpr2`, providing the output on `outExpr`.
 `inExpr1` and `inExpr2` should be of type [`ParticleValue`](@ref), `outExpr` will be of type `Float64`.
 """
-function get_expression(
-    ::ComputeTaskS2,
-    inExprs::Vector{String},
-    outExpr::String,
-)
-    return Meta.parse(
-        "$outExpr = compute(ComputeTaskS2(), $(inExprs[1]), $(inExprs[2]))",
-    )
+function get_expression(::ComputeTaskS2, inExprs::Vector{String}, outExpr::String)
+    return Meta.parse("$outExpr = compute(ComputeTaskS2(), $(inExprs[1]), $(inExprs[2]))")
 end
 
 """
@@ -152,11 +130,7 @@ end
 Generate code evaluating [`ComputeTaskS1`](@ref) on `inExpr`, providing the output on `outExpr`.
 `inExpr` should be of type [`ParticleValue`](@ref), `outExpr` will be of type [`ParticleValue`](@ref).
 """
-function get_expression(
-    ::ComputeTaskS1,
-    inExprs::Vector{String},
-    outExpr::String,
-)
+function get_expression(::ComputeTaskS1, inExprs::Vector{String}, outExpr::String)
     return Meta.parse("$outExpr = compute(ComputeTaskS1(), $(inExprs[1]))")
 end
 
@@ -166,14 +140,8 @@ end
 Generate code evaluating [`ComputeTaskSum`](@ref) on `inExprs`, providing the output on `outExpr`.
 `inExprs` should be of type [`Float64`], `outExpr` will be of type [`Float64`].
 """
-function get_expression(
-    ::ComputeTaskSum,
-    inExprs::Vector{String},
-    outExpr::String,
-)
-    return Meta.parse(
-        "$outExpr = compute(ComputeTaskSum(), [$(unroll_string_vector(inExprs))])",
-    )
+function get_expression(::ComputeTaskSum, inExprs::Vector{String}, outExpr::String)
+    return Meta.parse("$outExpr = compute(ComputeTaskSum(), [$(unroll_string_vector(inExprs))])")
 end
 
 """
@@ -182,19 +150,14 @@ end
 Generate code evaluating a [`FusedComputeTask`](@ref) on `inExprs`, providing the output on `outExpr`.
 `inExprs` should be of the correct types and may be heterogeneous. `outExpr` will be of the type of the output of `T2` of t.
 """
-function get_expression(
-    t::FusedComputeTask,
-    inExprs::Vector{String},
-    outExpr::String,
-)
+function get_expression(t::FusedComputeTask, inExprs::Vector{String}, outExpr::String)
     c1 = length(t.t1_inputs)
     c2 = length(t.t2_inputs) + 1
     expr1 = nothing
     expr2 = nothing
 
     expr1 = get_expression(t.first_task, t.t1_inputs, t.t1_output)
-    expr2 =
-        get_expression(t.second_task, [t.t2_inputs..., t.t1_output], outExpr)
+    expr2 = get_expression(t.second_task, [t.t2_inputs..., t.t1_output], outExpr)
 
     full_expr = Expr(:block, expr1, expr2)
 
@@ -208,7 +171,7 @@ Generate and return code for a given [`ComputeTaskNode`](@ref).
 """
 function get_expression(node::ComputeTaskNode)
     t = typeof(node.task)
-    @assert length(node.children) == children(node.task) "Node $(node) has inconsistent number of children"
+    @assert length(node.children) >= children(node.task) "Node $(node) has too few children for its task: node has $(length(node.children)) versus task has $(children(node.task))\nNode's children: $(getfield.(node.children, :children))"
 
     if (t <: ComputeTaskU || t <: ComputeTaskP || t <: ComputeTaskS1) # single input
         symbolIn = "data_$(to_var_name(node.children[1].id))"
diff --git a/src/models/abc/create.jl b/src/models/abc/create.jl
index 90b2da3..9dc2ca1 100644
--- a/src/models/abc/create.jl
+++ b/src/models/abc/create.jl
@@ -6,51 +6,61 @@ using ForwardDiff
 ComputeTaskSum() = ComputeTaskSum(0)
 
 """
-    gen_particles(in::Vector{ParticleType}, out::Vector{ParticleType})
+    gen_process_input(in::Vector{ParticleType}, out::Vector{ParticleType})
 
-Return a Vector of randomly generated [`Particle`](@ref)s. `in` is the list of particles that enter the process, `out` the list of particles that exit it. Their added momenta will be equal.
+Return a ProcessInput of randomly generated [`ABCParticle`](@ref)s from a [`ABCProcessDescription`](@ref). The process description can be created manually or parsed from a string using [`parse_process`](@ref).
 
-Note: This does not take into account the preservation of momenta required for an actual valid process!
+Note: This uses RAMBO to create a valid process with conservation of momentum and energy.
 """
-function gen_particles(
-    in_particles::Vector{ParticleType},
-    out_particles::Vector{ParticleType},
-)
-    particles = Dict{ParticleType, Vector{Particle}}()
-    rng = MersenneTwister(0)
+function gen_process_input(processDescription::ABCProcessDescription)
+    inParticleTypes = keys(processDescription.inParticles)
+    outParticleTypes = keys(processDescription.outParticles)
 
-    mass_sum = 0
-    input_masses = Vector{Float64}()
-    for particle in in_particles
-        mass_sum += mass(particle)
-        push!(input_masses, mass(particle))
+    massSum = 0
+    inputMasses = Vector{Float64}()
+    for (particle, n) in processDescription.inParticles
+        for _ in 1:n
+            massSum += mass(particle)
+            push!(inputMasses, mass(particle))
+        end
     end
-    output_masses = Vector{Float64}()
-    for particle in out_particles
-        mass_sum += mass(particle)
-        push!(output_masses, mass(particle))
+    outputMasses = Vector{Float64}()
+    for (particle, n) in processDescription.outParticles
+        for _ in 1:n
+            massSum += mass(particle)
+            push!(outputMasses, mass(particle))
+        end
     end
 
     # add some extra random mass to allow for some momentum
-    mass_sum += rand(rng) * (length(in_particles) + length(out_particles))
+    massSum += rand(rng[threadid()]) * (length(inputMasses) + length(outputMasses))
 
 
-    input_particles = Vector{Particle}()
-    initial_momenta = generate_initial_moms(mass_sum, input_masses)
-    for (mom, type) in zip(initial_momenta, in_particles)
-        push!(input_particles, Particle(mom, type))
+    inputParticles = Vector{ABCParticle}()
+    initialMomenta = generate_initial_moms(massSum, inputMasses)
+    index = 1
+    for (particle, n) in processDescription.inParticles
+        for _ in 1:n
+            mom = initialMomenta[index]
+            push!(inputParticles, particle(mom))
+            index += 1
+        end
     end
 
-    output_particles = Vector{Particle}()
-    final_momenta = generate_physical_massive_moms(rng, mass_sum, output_masses)
-    for (mom, type) in zip(final_momenta, out_particles)
-        push!(
-            output_particles,
-            Particle(SFourMomentum(-mom.E, mom.px, mom.py, mom.pz), type),
-        )
+    outputParticles = Vector{ABCParticle}()
+    final_momenta = generate_physical_massive_moms(rng[threadid()], massSum, outputMasses)
+    index = 1
+    for (particle, n) in processDescription.outParticles
+        for _ in 1:n
+            mom = final_momenta[index]
+            push!(outputParticles, particle(SFourMomentum(-mom.E, mom.px, mom.py, mom.pz)))
+            index += 1
+        end
     end
 
-    return (input_particles, output_particles)
+    processInput = ABCProcessInput(processDescription, inputParticles, outputParticles)
+
+    return return processInput
 end
 
 ####################
@@ -75,12 +85,8 @@ function generate_initial_moms(ss, masses)
 end
 
 
-Random.rand(rng::AbstractRNG, ::Random.SamplerType{SFourMomentum}) =
-    SFourMomentum(rand(rng, 4))
-Random.rand(
-    rng::AbstractRNG,
-    ::Random.SamplerType{NTuple{N, Float64}},
-) where {N} = Tuple(rand(rng, N))
+Random.rand(rng::AbstractRNG, ::Random.SamplerType{SFourMomentum}) = SFourMomentum(rand(rng, 4))
+Random.rand(rng::AbstractRNG, ::Random.SamplerType{NTuple{N, Float64}}) where {N} = Tuple(rand(rng, N))
 
 
 function _transform_uni_to_mom(u1, u2, u3, u4)
@@ -109,8 +115,7 @@ function _transform_uni_to_mom!(uni_mom, dest)
 end
 
 _transform_uni_to_mom(u1234::Tuple) = _transform_uni_to_mom(u1234...)
-_transform_uni_to_mom(u1234::SFourMomentum) =
-    _transform_uni_to_mom(Tuple(u1234))
+_transform_uni_to_mom(u1234::SFourMomentum) = _transform_uni_to_mom(Tuple(u1234))
 
 function generate_massless_moms(rng, n::Int)
     a = Vector{SFourMomentum}(undef, n)
diff --git a/src/models/abc/parse.jl b/src/models/abc/parse.jl
index 1416115..eeb313a 100644
--- a/src/models/abc/parse.jl
+++ b/src/models/abc/parse.jl
@@ -32,13 +32,13 @@ function parse_edges(input::AbstractString)
 end
 
 """
-    parse_abc(filename::String; verbose::Bool = false)
+    parse_dag(filename::String, model::ABCModel; verbose::Bool = false)
 
 Read an abc-model process from the given file. If `verbose` is set to true, print some progress information to stdout.
 
 Returns a valid [`DAG`](@ref).
 """
-function parse_abc(filename::String, verbose::Bool = false)
+function parse_dag(filename::AbstractString, model::ABCModel, verbose::Bool = false)
     file = open(filename, "r")
 
     if (verbose)
@@ -63,25 +63,9 @@ function parse_abc(filename::String, verbose::Bool = false)
     end
     sizehint!(graph.nodes, estimate_no_nodes)
 
-    sum_node = insert_node!(
-        graph,
-        make_node(ComputeTaskSum(0)),
-        track = false,
-        invalidate_cache = false,
-    )
-    global_data_out = insert_node!(
-        graph,
-        make_node(DataTask(FLOAT_SIZE)),
-        track = false,
-        invalidate_cache = false,
-    )
-    insert_edge!(
-        graph,
-        sum_node,
-        global_data_out,
-        track = false,
-        invalidate_cache = false,
-    )
+    sum_node = insert_node!(graph, make_node(ComputeTaskSum(0)), track = false, invalidate_cache = false)
+    global_data_out = insert_node!(graph, make_node(DataTask(FLOAT_SIZE)), track = false, invalidate_cache = false)
+    insert_edge!(graph, sum_node, global_data_out, track = false, invalidate_cache = false)
 
     # remember the data out nodes for connection
     dataOutNodes = Dict()
@@ -96,10 +80,7 @@ function parse_abc(filename::String, verbose::Bool = false)
         noNodes += 1
         if (noNodes % 100 == 0)
             if (verbose)
-                percent = string(
-                    round(100.0 * noNodes / nodesToRead, digits = 2),
-                    "%",
-                )
+                percent = string(round(100.0 * noNodes / nodesToRead, digits = 2), "%")
                 print("\rReading Nodes... $percent")
             end
         end
@@ -111,59 +92,17 @@ function parse_abc(filename::String, verbose::Bool = false)
                 track = false,
                 invalidate_cache = false,
             ) # read particle data node
-            compute_P = insert_node!(
-                graph,
-                make_node(ComputeTaskP()),
-                track = false,
-                invalidate_cache = false,
-            ) # compute P node
-            data_Pu = insert_node!(
-                graph,
-                make_node(DataTask(PARTICLE_VALUE_SIZE)),
-                track = false,
-                invalidate_cache = false,
-            ) # transfer data from P to u (one ParticleValue object)
-            compute_u = insert_node!(
-                graph,
-                make_node(ComputeTaskU()),
-                track = false,
-                invalidate_cache = false,
-            ) # compute U node
-            data_out = insert_node!(
-                graph,
-                make_node(DataTask(PARTICLE_VALUE_SIZE)),
-                track = false,
-                invalidate_cache = false,
-            ) # transfer data out from u (one ParticleValue object)
+            compute_P = insert_node!(graph, make_node(ComputeTaskP()), track = false, invalidate_cache = false) # compute P node
+            data_Pu =
+                insert_node!(graph, make_node(DataTask(PARTICLE_VALUE_SIZE)), track = false, invalidate_cache = false) # transfer data from P to u (one ParticleValue object)
+            compute_u = insert_node!(graph, make_node(ComputeTaskU()), track = false, invalidate_cache = false) # compute U node
+            data_out =
+                insert_node!(graph, make_node(DataTask(PARTICLE_VALUE_SIZE)), track = false, invalidate_cache = false) # transfer data out from u (one ParticleValue object)
 
-            insert_edge!(
-                graph,
-                data_in,
-                compute_P,
-                track = false,
-                invalidate_cache = false,
-            )
-            insert_edge!(
-                graph,
-                compute_P,
-                data_Pu,
-                track = false,
-                invalidate_cache = false,
-            )
-            insert_edge!(
-                graph,
-                data_Pu,
-                compute_u,
-                track = false,
-                invalidate_cache = false,
-            )
-            insert_edge!(
-                graph,
-                compute_u,
-                data_out,
-                track = false,
-                invalidate_cache = false,
-            )
+            insert_edge!(graph, data_in, compute_P, track = false, invalidate_cache = false)
+            insert_edge!(graph, compute_P, data_Pu, track = false, invalidate_cache = false)
+            insert_edge!(graph, data_Pu, compute_u, track = false, invalidate_cache = false)
+            insert_edge!(graph, compute_u, data_out, track = false, invalidate_cache = false)
 
             # remember the data_out node for future edges
             dataOutNodes[node] = data_out
@@ -173,27 +112,13 @@ function parse_abc(filename::String, verbose::Bool = false)
             in1 = capt.captures[1]
             in2 = capt.captures[2]
 
-            compute_v = insert_node!(
-                graph,
-                make_node(ComputeTaskV()),
-                track = false,
-                invalidate_cache = false,
-            )
-            data_out = insert_node!(
-                graph,
-                make_node(DataTask(PARTICLE_VALUE_SIZE)),
-                track = false,
-                invalidate_cache = false,
-            )
+            compute_v = insert_node!(graph, make_node(ComputeTaskV()), track = false, invalidate_cache = false)
+            data_out =
+                insert_node!(graph, make_node(DataTask(PARTICLE_VALUE_SIZE)), track = false, invalidate_cache = false)
 
             if (occursin(regex_c, in1))
                 # put an S node after this input
-                compute_S = insert_node!(
-                    graph,
-                    make_node(ComputeTaskS1()),
-                    track = false,
-                    invalidate_cache = false,
-                )
+                compute_S = insert_node!(graph, make_node(ComputeTaskS1()), track = false, invalidate_cache = false)
                 data_S_v = insert_node!(
                     graph,
                     make_node(DataTask(PARTICLE_VALUE_SIZE)),
@@ -201,47 +126,18 @@ function parse_abc(filename::String, verbose::Bool = false)
                     invalidate_cache = false,
                 )
 
-                insert_edge!(
-                    graph,
-                    dataOutNodes[in1],
-                    compute_S,
-                    track = false,
-                    invalidate_cache = false,
-                )
-                insert_edge!(
-                    graph,
-                    compute_S,
-                    data_S_v,
-                    track = false,
-                    invalidate_cache = false,
-                )
+                insert_edge!(graph, dataOutNodes[in1], compute_S, track = false, invalidate_cache = false)
+                insert_edge!(graph, compute_S, data_S_v, track = false, invalidate_cache = false)
 
-                insert_edge!(
-                    graph,
-                    data_S_v,
-                    compute_v,
-                    track = false,
-                    invalidate_cache = false,
-                )
+                insert_edge!(graph, data_S_v, compute_v, track = false, invalidate_cache = false)
             else
-                insert_edge!(
-                    graph,
-                    dataOutNodes[in1],
-                    compute_v,
-                    track = false,
-                    invalidate_cache = false,
-                )
+                insert_edge!(graph, dataOutNodes[in1], compute_v, track = false, invalidate_cache = false)
             end
 
             if (occursin(regex_c, in2))
                 # i think the current generator only puts the combined particles in the first space, so this case might never be entered
                 # put an S node after this input
-                compute_S = insert_node!(
-                    graph,
-                    make_node(ComputeTaskS1()),
-                    track = false,
-                    invalidate_cache = false,
-                )
+                compute_S = insert_node!(graph, make_node(ComputeTaskS1()), track = false, invalidate_cache = false)
                 data_S_v = insert_node!(
                     graph,
                     make_node(DataTask(PARTICLE_VALUE_SIZE)),
@@ -249,45 +145,15 @@ function parse_abc(filename::String, verbose::Bool = false)
                     invalidate_cache = false,
                 )
 
-                insert_edge!(
-                    graph,
-                    dataOutNodes[in2],
-                    compute_S,
-                    track = false,
-                    invalidate_cache = false,
-                )
-                insert_edge!(
-                    graph,
-                    compute_S,
-                    data_S_v,
-                    track = false,
-                    invalidate_cache = false,
-                )
+                insert_edge!(graph, dataOutNodes[in2], compute_S, track = false, invalidate_cache = false)
+                insert_edge!(graph, compute_S, data_S_v, track = false, invalidate_cache = false)
 
-                insert_edge!(
-                    graph,
-                    data_S_v,
-                    compute_v,
-                    track = false,
-                    invalidate_cache = false,
-                )
+                insert_edge!(graph, data_S_v, compute_v, track = false, invalidate_cache = false)
             else
-                insert_edge!(
-                    graph,
-                    dataOutNodes[in2],
-                    compute_v,
-                    track = false,
-                    invalidate_cache = false,
-                )
+                insert_edge!(graph, dataOutNodes[in2], compute_v, track = false, invalidate_cache = false)
             end
 
-            insert_edge!(
-                graph,
-                compute_v,
-                data_out,
-                track = false,
-                invalidate_cache = false,
-            )
+            insert_edge!(graph, compute_v, data_out, track = false, invalidate_cache = false)
             dataOutNodes[node] = data_out
 
         elseif occursin(regex_m, node)
@@ -298,84 +164,23 @@ function parse_abc(filename::String, verbose::Bool = false)
             in3 = capt.captures[3]
 
             # in2 + in3 with a v
-            compute_v = insert_node!(
-                graph,
-                make_node(ComputeTaskV()),
-                track = false,
-                invalidate_cache = false,
-            )
-            data_v = insert_node!(
-                graph,
-                make_node(DataTask(PARTICLE_VALUE_SIZE)),
-                track = false,
-                invalidate_cache = false,
-            )
+            compute_v = insert_node!(graph, make_node(ComputeTaskV()), track = false, invalidate_cache = false)
+            data_v =
+                insert_node!(graph, make_node(DataTask(PARTICLE_VALUE_SIZE)), track = false, invalidate_cache = false)
 
-            insert_edge!(
-                graph,
-                dataOutNodes[in2],
-                compute_v,
-                track = false,
-                invalidate_cache = false,
-            )
-            insert_edge!(
-                graph,
-                dataOutNodes[in3],
-                compute_v,
-                track = false,
-                invalidate_cache = false,
-            )
-            insert_edge!(
-                graph,
-                compute_v,
-                data_v,
-                track = false,
-                invalidate_cache = false,
-            )
+            insert_edge!(graph, dataOutNodes[in2], compute_v, track = false, invalidate_cache = false)
+            insert_edge!(graph, dataOutNodes[in3], compute_v, track = false, invalidate_cache = false)
+            insert_edge!(graph, compute_v, data_v, track = false, invalidate_cache = false)
 
             # combine with the v of the combined other input
-            compute_S2 = insert_node!(
-                graph,
-                make_node(ComputeTaskS2()),
-                track = false,
-                invalidate_cache = false,
-            )
-            data_out = insert_node!(
-                graph,
-                make_node(DataTask(FLOAT_SIZE)),
-                track = false,
-                invalidate_cache = false,
-            ) # output of a S2 task is only a float
+            compute_S2 = insert_node!(graph, make_node(ComputeTaskS2()), track = false, invalidate_cache = false)
+            data_out = insert_node!(graph, make_node(DataTask(FLOAT_SIZE)), track = false, invalidate_cache = false) # output of a S2 task is only a float
 
-            insert_edge!(
-                graph,
-                data_v,
-                compute_S2,
-                track = false,
-                invalidate_cache = false,
-            )
-            insert_edge!(
-                graph,
-                dataOutNodes[in1],
-                compute_S2,
-                track = false,
-                invalidate_cache = false,
-            )
-            insert_edge!(
-                graph,
-                compute_S2,
-                data_out,
-                track = false,
-                invalidate_cache = false,
-            )
+            insert_edge!(graph, data_v, compute_S2, track = false, invalidate_cache = false)
+            insert_edge!(graph, dataOutNodes[in1], compute_S2, track = false, invalidate_cache = false)
+            insert_edge!(graph, compute_S2, data_out, track = false, invalidate_cache = false)
 
-            insert_edge!(
-                graph,
-                data_out,
-                sum_node,
-                track = false,
-                invalidate_cache = false,
-            )
+            insert_edge!(graph, data_out, sum_node, track = false, invalidate_cache = false)
             add_child!(sum_node.task)
         elseif occursin(regex_plus, node)
             if (verbose)
@@ -383,9 +188,7 @@ function parse_abc(filename::String, verbose::Bool = false)
                 println("Added ", length(graph.nodes), " nodes")
             end
         else
-            @assert false (
-                "Unknown node '$node' while reading from file $filename"
-            )
+            @assert false ("Unknown node '$node' while reading from file $filename")
         end
     end
 
@@ -404,3 +207,36 @@ function parse_abc(filename::String, verbose::Bool = false)
     # don't actually need to read the edges
     return graph
 end
+
+"""
+    parse_process(string::AbstractString, model::ABCModel)
+
+Parse a string representation of a process, such as "AB->ABBB" into the corresponding [`ABCProcessDescription`](@ref).
+"""
+function parse_process(str::AbstractString, model::ABCModel)
+    inParticles = Dict{Type, Int}()
+    outParticles = Dict{Type, Int}()
+
+    if !(contains(str, "->"))
+        throw("Did not find -> while parsing process \"$str\"")
+    end
+
+    (inStr, outStr) = split(str, "->")
+
+    if (isempty(inStr) || isempty(outStr))
+        throw("Process (\"$str\") input or output part is empty!")
+    end
+
+    for t in types(model)
+        inParticles[t] = count(x -> x == String(t)[1], inStr)
+        outParticles[t] = count(x -> x == String(t)[1], outStr)
+    end
+
+    if length(inStr) != sum(values(inParticles))
+        throw("Encountered unknown characters in the input part of process \"$str\"")
+    elseif length(outStr) != sum(values(outParticles))
+        throw("Encountered unknown characters in the output part of process \"$str\"")
+    end
+
+    return ABCProcessDescription(inParticles, outParticles)
+end
diff --git a/src/models/abc/particle.jl b/src/models/abc/particle.jl
index 99a7dc4..0527124 100644
--- a/src/models/abc/particle.jl
+++ b/src/models/abc/particle.jl
@@ -1,108 +1,106 @@
 using QEDbase
 
-"""
-    ParticleType
+abstract type ABCParticle <: AbstractParticle end
+
+struct ABCModel <: AbstractPhysicsModel end
+
+struct ABCProcessDescription <: AbstractProcessDescription
+    inParticles::Dict{Type, Int}
+    outParticles::Dict{Type, Int}
+end
+
+struct ABCProcessInput <: AbstractProcessInput
+    process::ABCProcessDescription
+    inParticles::Vector{ABCParticle}
+    outParticles::Vector{ABCParticle}
+end
 
-A Particle Type in the ABC Model as an enum, with types `A`, `B` and `C`.
 """
-@enum ParticleType A = 1 B = 2 C = 3
+    ParticleA <: ABCParticle
+
+An 'A' particle in the ABC Model.
+"""
+struct ParticleA <: ABCParticle
+    momentum::SFourMomentum
+end
+
+struct ParticleB <: ABCParticle
+    momentum::SFourMomentum
+end
+
+struct ParticleC <: ABCParticle
+    momentum::SFourMomentum
+end
 
 """
     PARTICLE_MASSES
 
 A constant dictionary containing the masses of the different [`ParticleType`](@ref)s.
 """
-const PARTICLE_MASSES =
-    Dict{ParticleType, Float64}(A => 1.0, B => 1.0, C => 0.0)
+const PARTICLE_MASSES = Dict{Type, Float64}(ParticleA => 1.0, ParticleB => 1.0, ParticleC => 0.0)
 
 """
-    Particle
-
-A struct describing a particle of the ABC-Model. It has the 4 momentum of the particle and a [`ParticleType`](@ref).
-
-`sizeof(Particle())` = 40 Byte
-"""
-struct Particle
-    # SFourMomentum
-    momentum::SFourMomentum
-
-    type::ParticleType
-end
-
-"""
-    ParticleValue
-
-A struct describing a particle during a calculation of a Feynman Diagram, together with the value that's being calculated.
-
-`sizeof(ParticleValue())` = 48 Byte
-"""
-struct ParticleValue
-    p::Particle
-    v::Float64
-end
-
-"""
-    mass(t::ParticleType)
+    mass(t::Type{T}) where {T <: ABCParticle}
     
 Return the mass (at rest) of the given particle type.
 """
-mass(t::ParticleType) = PARTICLE_MASSES[t]
+mass(t::Type{T}) where {T <: ABCParticle} = PARTICLE_MASSES[t]
 
 """
-    remaining_type(t1::ParticleType, t2::ParticleType)
+    interaction_result(t1::Type{T1}, t2::Type{T2}) where {T1 <: ABCParticle, T2 <: ABCParticle}
 
 For 2 given (non-equal) particle types, return the third of ABC.
 """
-function remaining_type(t1::ParticleType, t2::ParticleType)
+function interaction_result(t1::Type{T1}, t2::Type{T2}) where {T1 <: ABCParticle, T2 <: ABCParticle}
     @assert t1 != t2
-    if t1 != A && t2 != A
-        return A
-    elseif t1 != B && t2 != B
-        return B
+    if t1 != Type{ParticleA} && t2 != Type{ParticleA}
+        return ParticleA
+    elseif t1 != Type{ParticleB} && t2 != Type{ParticleB}
+        return ParticleB
     else
-        return C
+        return ParticleC
     end
 end
 
 """
-    types(::Particle)
+    types(::ABCModel)
 
-Return a Vector of the possible [`ParticleType`](@ref)s of this [`Particle`](@ref).
+Return a Vector of the possible types of particle in the [`ABCModel`](@ref).
 """
-function types(::Particle)
-    return [A, B, C]
+function types(::ABCModel)
+    return [ParticleA, ParticleB, ParticleC]
 end
 
 """
-    square(p::Particle)
+    square(p::ABCParticle)
 
 Return the square of the particle's momentum as a `Float` value.
 
 Takes 7 effective FLOP.
 """
-function square(p::Particle)
+function square(p::ABCParticle)
     return getMass2(p.momentum)
 end
 
 """
-    inner_edge(p::Particle)
+    inner_edge(p::ABCParticle)
 
 Return the factor of the inner edge with the given (virtual) particle.
 
 Takes 10 effective FLOP. (3 here + 7 in square(p))
 """
-function inner_edge(p::Particle)
-    return 1.0 / (square(p) - mass(p.type) * mass(p.type))
+function inner_edge(p::ABCParticle)
+    return 1.0 / (square(p) - mass(typeof(p)) * mass(typeof(p)))
 end
 
 """
-    outer_edge(p::Particle)
+    outer_edge(p::ABCParticle)
 
 Return the factor of the outer edge with the given (real) particle.
 
 Takes 0 effective FLOP.
 """
-function outer_edge(p::Particle)
+function outer_edge(p::ABCParticle)
     return 1.0
 end
 
@@ -120,14 +118,15 @@ function vertex()
 end
 
 """
-    preserve_momentum(p1::Particle, p2::Particle)
+    preserve_momentum(p1::ABCParticle, p2::ABCParticle)
 
 Calculate and return a new particle from two given interacting ones at a vertex.
 
 Takes 4 effective FLOP.
 """
-function preserve_momentum(p1::Particle, p2::Particle)
-    p3 = Particle(p1.momentum + p2.momentum, remaining_type(p1.type, p2.type))
+function preserve_momentum(p1::ABCParticle, p2::ABCParticle)
+    t3 = interaction_result(typeof(p1), typeof(p2))
+    p3 = t3(p1.momentum + p2.momentum)
 
     return p3
 end
@@ -135,16 +134,42 @@ end
 """
     type_from_name(name::String)
 
-For a name of a particle, return the [`ParticleType`].
+For a name of a particle, return the particle's [`Type`].
 """
 function type_from_name(name::String)
     if startswith(name, "A")
-        return A
+        return ParticleA
     elseif startswith(name, "B")
-        return B
+        return ParticleB
     elseif startswith(name, "C")
-        return C
+        return ParticleC
     else
         throw("Invalid name for a particle in the ABC model")
     end
 end
+
+function String(::Type{ParticleA})
+    return "A"
+end
+function String(::Type{ParticleB})
+    return "B"
+end
+function String(::Type{ParticleC})
+    return "C"
+end
+
+function in_particles(process::ABCProcessDescription)
+    return process.inParticles
+end
+
+function in_particles(input::ABCProcessInput)
+    return input.inParticles
+end
+
+function out_particles(process::ABCProcessDescription)
+    return process.outParticles
+end
+
+function out_particles(input::ABCProcessInput)
+    return input.outParticles
+end
diff --git a/src/models/abc/types.jl b/src/models/abc/types.jl
index 2f917cb..e9e6ee9 100644
--- a/src/models/abc/types.jl
+++ b/src/models/abc/types.jl
@@ -56,12 +56,4 @@ end
 
 Constant vector of all tasks of the ABC-Model.
 """
-ABC_TASKS = [
-    DataTask,
-    ComputeTaskS1,
-    ComputeTaskS2,
-    ComputeTaskP,
-    ComputeTaskV,
-    ComputeTaskU,
-    ComputeTaskSum,
-]
+ABC_TASKS = [DataTask, ComputeTaskS1, ComputeTaskS2, ComputeTaskP, ComputeTaskV, ComputeTaskU, ComputeTaskSum]
diff --git a/src/models/interface.jl b/src/models/interface.jl
new file mode 100644
index 0000000..3297f4c
--- /dev/null
+++ b/src/models/interface.jl
@@ -0,0 +1,109 @@
+
+"""
+    AbstractPhysicsModel
+
+Base type for a model, e.g. ABC-Model or QED. This is used to dispatch many functions.
+"""
+abstract type AbstractPhysicsModel end
+
+"""
+    AbstractParticle
+
+Base type for particles belonging to a certain [`AbstractPhysicsModel`](@ref).
+"""
+abstract type AbstractParticle end
+
+"""
+    ParticleValue{ParticleType <: AbstractParticle}
+
+A struct describing a particle during a calculation of a Feynman Diagram, together with the value that's being calculated.
+
+`sizeof(ParticleValue())` = 48 Byte
+"""
+struct ParticleValue{ParticleType <: AbstractParticle}
+    p::ParticleType
+    v::Float64
+end
+
+"""
+    AbstractProcessDescription
+
+Base type for process descriptions. An object of this type of a corresponding [`AbstractPhysicsModel`](@ref) should uniquely identify a process in that model.
+
+See also: [`parse_process`](@ref)
+"""
+abstract type AbstractProcessDescription end
+
+"""
+    AbstractProcessInput
+
+Base type for process inputs. An object of this type contains the input values (e.g. momenta) of the particles in a process.
+
+See also: [`gen_process_input`](@ref)
+"""
+abstract type AbstractProcessInput end
+
+"""
+    mass(t::Type{T}) where {T <: AbstractParticle}
+
+Interface function that must be implemented for every subtype of [`AbstractParticle`](@ref), returning the particles mass at rest.
+"""
+function mass end
+
+"""
+    interaction_result(t1::Type{T1}, t2::Type{T2}) where {T1 <: AbstractParticle, T2 <: AbstractParticle}
+
+Interface function that must be implemented for every subtype of [`AbstractParticle`](@ref), returning the result particle type when the two given particles interact.
+"""
+function interaction_result end
+
+"""
+    types(::AbstractPhysicsModel)
+
+Interface function that must be implemented for every subtype of [`AbstractPhysicsModel`](@ref), returning a `Vector` of the available particle types in the model.
+"""
+function types end
+
+"""
+    in_particles(::AbstractProcessDescription)
+
+Interface function that must be implemented for every subtype of [`AbstractProcessDescription`](@ref).
+Returns a `<: Dict{Type{AbstractParticle}, Int}` object, representing the number of ingoing particles for the process per particle type.
+
+
+    in_particles(::AbstractProcessInput)
+
+Interface function that must be implemented for every subtype of [`AbstractProcessInput`](@ref).
+Returns a `<: Vector{AbstractParticle}` object with the values of all ingoing particles for the corresponding `ProcessDescription`.
+"""
+function in_particles end
+
+"""
+    out_particles(::AbstractProcessDescription)
+
+Interface function that must be implemented for every subtype of [`AbstractProcessDescription`](@ref).
+Returns a `<: Dict{Type{AbstractParticle}, Int}` object, representing the number of outgoing particles for the process per particle type.
+
+
+    out_particles(::AbstractProcessInput)
+
+Interface function that must be implemented for every subtype of [`AbstractProcessInput`](@ref).
+Returns a `<: Vector{AbstractParticle}` object with the values of all outgoing particles for the corresponding `ProcessDescription`.
+"""
+function out_particles end
+
+"""
+    parse_process(::AbstractString, ::AbstractPhysicsModel)
+
+Interface function that must be implemented for every subtype of [`AbstractPhysicsModel`](@ref).
+Returns a `ProcessDescription` object.
+"""
+function parse_process end
+
+"""
+    gen_process_input(::AbstractProcessDescription)
+
+Interface function that must be implemented for every specific [`AbstractProcessDescription`](@ref).
+Returns a randomly generated and valid corresponding `ProcessInput`.
+"""
+function gen_process_input end
diff --git a/src/node/create.jl b/src/node/create.jl
index 524acaf..ee8682e 100644
--- a/src/node/create.jl
+++ b/src/node/create.jl
@@ -1,14 +1,6 @@
 
-DataTaskNode(t::AbstractDataTask, name = "") = DataTaskNode(
-    t,
-    Vector{Node}(),
-    Vector{Node}(),
-    UUIDs.uuid1(rng[threadid()]),
-    missing,
-    missing,
-    missing,
-    name,
-)
+DataTaskNode(t::AbstractDataTask, name = "") =
+    DataTaskNode(t, Vector{Node}(), Vector{Node}(), UUIDs.uuid1(rng[threadid()]), missing, missing, missing, name)
 ComputeTaskNode(t::AbstractComputeTask) = ComputeTaskNode(
     t,
     Vector{Node}(),
diff --git a/src/node/type.jl b/src/node/type.jl
index 06bf308..2fe028a 100644
--- a/src/node/type.jl
+++ b/src/node/type.jl
@@ -95,8 +95,5 @@ The child is the prerequisite node of the parent.
 """
 struct Edge
     # edge points from child to parent
-    edge::Union{
-        Tuple{DataTaskNode, ComputeTaskNode},
-        Tuple{ComputeTaskNode, DataTaskNode},
-    }
+    edge::Union{Tuple{DataTaskNode, ComputeTaskNode}, Tuple{ComputeTaskNode, DataTaskNode}}
 end
diff --git a/src/operation/apply.jl b/src/operation/apply.jl
index 1df19fd..87bea22 100644
--- a/src/operation/apply.jl
+++ b/src/operation/apply.jl
@@ -34,12 +34,7 @@ Apply the given [`NodeFusion`](@ref) to the graph. Generic wrapper around [`node
 Return an [`AppliedNodeFusion`](@ref) object generated from the graph's [`Diff`](@ref).
 """
 function apply_operation!(graph::DAG, operation::NodeFusion)
-    diff = node_fusion!(
-        graph,
-        operation.input[1],
-        operation.input[2],
-        operation.input[3],
-    )
+    diff = node_fusion!(graph, operation.input[1], operation.input[2], operation.input[3])
 
     graph.properties += GraphProperties(diff)
 
@@ -156,12 +151,7 @@ Fuse nodes n1 -> n2 -> n3 together into one node, return the applied difference
 
 For details see [`NodeFusion`](@ref).
 """
-function node_fusion!(
-    graph::DAG,
-    n1::ComputeTaskNode,
-    n2::DataTaskNode,
-    n3::ComputeTaskNode,
-)
+function node_fusion!(graph::DAG, n1::ComputeTaskNode, n2::DataTaskNode, n3::ComputeTaskNode)
     @assert is_valid_node_fusion_input(graph, n1, n2, n3)
 
     # clear snapshot
@@ -193,15 +183,7 @@ function node_fusion!(
     end
 
     # create new node with the fused compute task
-    new_node = ComputeTaskNode(
-        FusedComputeTask(
-            n1.task,
-            n3.task,
-            n1_inputs,
-            "data_$(to_var_name(n2.id))",
-            n3_inputs,
-        ),
-    )
+    new_node = ComputeTaskNode(FusedComputeTask(n1.task, n3.task, n1_inputs, "data_$(to_var_name(n2.id))", n3_inputs))
     insert_node!(graph, new_node)
 
     for child in n1_children
@@ -222,12 +204,7 @@ function node_fusion!(
 
         # important! update the parent node's child names in case they are fused compute tasks
         # needed for compute generation so the fused compute task can correctly match inputs to its component tasks
-        update_child!(
-            graph,
-            parent,
-            "data_$(to_var_name(n3.id))",
-            "data_$(to_var_name(new_node.id))",
-        )
+        update_child!(graph, parent, "data_$(to_var_name(n3.id))", "data_$(to_var_name(new_node.id))")
     end
 
     return get_snapshot_diff(graph)
@@ -327,12 +304,7 @@ function node_split!(graph::DAG, n1::Node)
             insert_edge!(graph, child, n_copy)
         end
 
-        update_child!(
-            graph,
-            parent,
-            "data_$(to_var_name(n1.id))",
-            "data_$(to_var_name(n_copy.id))",
-        )
+        update_child!(graph, parent, "data_$(to_var_name(n1.id))", "data_$(to_var_name(n_copy.id))")
     end
 
     return get_snapshot_diff(graph)
diff --git a/src/operation/find.jl b/src/operation/find.jl
index 89acc3a..f6d6218 100644
--- a/src/operation/find.jl
+++ b/src/operation/find.jl
@@ -7,10 +7,7 @@ using Base.Threads
 
 Insert the given node fusion into its input nodes' operation caches. For the compute nodes, locking via the given `locks` is employed to have safe multi-threading. For a large set of nodes, contention on the locks should be very small.
 """
-function insert_operation!(
-    nf::NodeFusion,
-    locks::Dict{ComputeTaskNode, SpinLock},
-)
+function insert_operation!(nf::NodeFusion, locks::Dict{ComputeTaskNode, SpinLock})
     n1 = nf.input[1]
     n2 = nf.input[2]
     n3 = nf.input[3]
@@ -52,10 +49,7 @@ end
 
 Insert the node reductions into the graph and the nodes' caches. Employs multithreading for speedup.
 """
-function nr_insertion!(
-    operations::PossibleOperations,
-    nodeReductions::Vector{Vector{NodeReduction}},
-)
+function nr_insertion!(operations::PossibleOperations, nodeReductions::Vector{Vector{NodeReduction}})
     total_len = 0
     for vec in nodeReductions
         total_len += length(vec)
@@ -83,11 +77,7 @@ end
 
 Insert the node fusions into the graph and the nodes' caches. Employs multithreading for speedup.
 """
-function nf_insertion!(
-    graph::DAG,
-    operations::PossibleOperations,
-    nodeFusions::Vector{Vector{NodeFusion}},
-)
+function nf_insertion!(graph::DAG, operations::PossibleOperations, nodeFusions::Vector{Vector{NodeFusion}})
     total_len = 0
     for vec in nodeFusions
         total_len += length(vec)
@@ -122,10 +112,7 @@ end
 
 Insert the node splits into the graph and the nodes' caches. Employs multithreading for speedup.
 """
-function ns_insertion!(
-    operations::PossibleOperations,
-    nodeSplits::Vector{Vector{NodeSplit}},
-)
+function ns_insertion!(operations::PossibleOperations, nodeSplits::Vector{Vector{NodeSplit}})
     total_len = 0
     for vec in nodeSplits
         total_len += length(vec)
@@ -231,16 +218,12 @@ function generate_operations(graph::DAG)
                 continue
             end
 
-            push!(
-                generatedFusions[threadid()],
-                NodeFusion((child_node, node, parent_node)),
-            )
+            push!(generatedFusions[threadid()], NodeFusion((child_node, node, parent_node)))
         end
     end
 
     # launch thread for node fusion insertion
-    nf_task =
-        @task nf_insertion!(graph, graph.possibleOperations, generatedFusions)
+    nf_task = @task nf_insertion!(graph, graph.possibleOperations, generatedFusions)
     schedule(nf_task)
 
     # find possible node splits
diff --git a/src/operation/utility.jl b/src/operation/utility.jl
index 2c1bae5..b7f874a 100644
--- a/src/operation/utility.jl
+++ b/src/operation/utility.jl
@@ -4,9 +4,7 @@
 Return whether `operations` is empty, i.e. all of its fields are empty.
 """
 function isempty(operations::PossibleOperations)
-    return isempty(operations.nodeFusions) &&
-           isempty(operations.nodeReductions) &&
-           isempty(operations.nodeSplits)
+    return isempty(operations.nodeFusions) && isempty(operations.nodeReductions) && isempty(operations.nodeSplits)
 end
 
 """
@@ -63,9 +61,7 @@ function can_fuse(n1::ComputeTaskNode, n2::DataTaskNode, n3::ComputeTaskNode)
         return false
     end
 
-    if length(n2.parents) != 1 ||
-       length(n2.children) != 1 ||
-       length(n1.parents) != 1
+    if length(n2.parents) != 1 || length(n2.children) != 1 || length(n1.parents) != 1
         return false
     end
 
diff --git a/src/operation/validate.jl b/src/operation/validate.jl
index 528d8be..a065032 100644
--- a/src/operation/validate.jl
+++ b/src/operation/validate.jl
@@ -9,24 +9,12 @@ Assert for a gven node fusion input whether the nodes can be fused. For the requ
 
 Intended for use with `@assert` or `@test`.
 """
-function is_valid_node_fusion_input(
-    graph::DAG,
-    n1::ComputeTaskNode,
-    n2::DataTaskNode,
-    n3::ComputeTaskNode,
-)
+function is_valid_node_fusion_input(graph::DAG, n1::ComputeTaskNode, n2::DataTaskNode, n3::ComputeTaskNode)
     if !(n1 in graph) || !(n2 in graph) || !(n3 in graph)
-        throw(
-            AssertionError(
-                "[Node Fusion] The given nodes are not part of the given graph",
-            ),
-        )
+        throw(AssertionError("[Node Fusion] The given nodes are not part of the given graph"))
     end
 
-    if !is_child(n1, n2) ||
-       !is_child(n2, n3) ||
-       !is_parent(n3, n2) ||
-       !is_parent(n2, n1)
+    if !is_child(n1, n2) || !is_child(n2, n3) || !is_parent(n3, n2) || !is_parent(n2, n1)
         throw(
             AssertionError(
                 "[Node Fusion] The given nodes are not connected by edges which is required for node fusion",
@@ -35,25 +23,13 @@ function is_valid_node_fusion_input(
     end
 
     if length(n2.parents) > 1
-        throw(
-            AssertionError(
-                "[Node Fusion] The given data node has more than one parent",
-            ),
-        )
+        throw(AssertionError("[Node Fusion] The given data node has more than one parent"))
     end
     if length(n2.children) > 1
-        throw(
-            AssertionError(
-                "[Node Fusion] The given data node has more than one child",
-            ),
-        )
+        throw(AssertionError("[Node Fusion] The given data node has more than one child"))
     end
     if length(n1.parents) > 1
-        throw(
-            AssertionError(
-                "[Node Fusion] The given n1 has more than one parent",
-            ),
-        )
+        throw(AssertionError("[Node Fusion] The given n1 has more than one parent"))
     end
 
     return true
@@ -69,31 +45,19 @@ Intended for use with `@assert` or `@test`.
 function is_valid_node_reduction_input(graph::DAG, nodes::Vector{Node})
     for n in nodes
         if n ∉ graph
-            throw(
-                AssertionError(
-                    "[Node Reduction] The given nodes are not part of the given graph",
-                ),
-            )
+            throw(AssertionError("[Node Reduction] The given nodes are not part of the given graph"))
         end
     end
 
     t = typeof(nodes[1].task)
     for n in nodes
         if typeof(n.task) != t
-            throw(
-                AssertionError(
-                    "[Node Reduction] The given nodes are not of the same type",
-                ),
-            )
+            throw(AssertionError("[Node Reduction] The given nodes are not of the same type"))
         end
 
         if (typeof(n) <: DataTaskNode)
             if (n.name != nodes[1].name)
-                throw(
-                    AssertionError(
-                        "[Node Reduction] The given nodes do not have the same name",
-                    ),
-                )
+                throw(AssertionError("[Node Reduction] The given nodes do not have the same name"))
             end
         end
     end
@@ -121,11 +85,7 @@ Intended for use with `@assert` or `@test`.
 """
 function is_valid_node_split_input(graph::DAG, n1::Node)
     if n1 ∉ graph
-        throw(
-            AssertionError(
-                "[Node Split] The given node is not part of the given graph",
-            ),
-        )
+        throw(AssertionError("[Node Split] The given node is not part of the given graph"))
     end
 
     if length(n1.parents) <= 1
@@ -173,12 +133,7 @@ Assert for a given [`NodeFusion`](@ref) whether it is a valid operation in the g
 Intended for use with `@assert` or `@test`.
 """
 function is_valid(graph::DAG, nf::NodeFusion)
-    @assert is_valid_node_fusion_input(
-        graph,
-        nf.input[1],
-        nf.input[2],
-        nf.input[3],
-    )
+    @assert is_valid_node_fusion_input(graph, nf.input[1], nf.input[2], nf.input[3])
     @assert nf in graph.possibleOperations.nodeFusions "NodeFusion is not part of the graph's possible operations!"
     return true
 end
diff --git a/src/properties/utility.jl b/src/properties/utility.jl
index bf936db..3aa9def 100644
--- a/src/properties/utility.jl
+++ b/src/properties/utility.jl
@@ -11,8 +11,7 @@ function -(prop1::GraphProperties, prop2::GraphProperties)
         computeIntensity = if (prop1.data - prop2.data == 0)
             0.0
         else
-            (prop1.computeEffort - prop2.computeEffort) /
-            (prop1.data - prop2.data)
+            (prop1.computeEffort - prop2.computeEffort) / (prop1.data - prop2.data)
         end,
         cost = prop1.cost - prop2.cost,
         noNodes = prop1.noNodes - prop2.noNodes,
@@ -33,8 +32,7 @@ function +(prop1::GraphProperties, prop2::GraphProperties)
         computeIntensity = if (prop1.data + prop2.data == 0)
             0.0
         else
-            (prop1.computeEffort + prop2.computeEffort) /
-            (prop1.data + prop2.data)
+            (prop1.computeEffort + prop2.computeEffort) / (prop1.data + prop2.data)
         end,
         cost = prop1.cost + prop2.cost,
         noNodes = prop1.noNodes + prop2.noNodes,
diff --git a/src/task/create.jl b/src/task/create.jl
index acbd128..81dc564 100644
--- a/src/task/create.jl
+++ b/src/task/create.jl
@@ -3,8 +3,7 @@
 
 Fallback implementation of the copy of an abstract data task, throwing an error.
 """
-copy(t::AbstractDataTask) =
-    error("Need to implement copying for your data tasks!")
+copy(t::AbstractDataTask) = error("Need to implement copying for your data tasks!")
 
 """
     copy(t::AbstractComputeTask)
@@ -28,9 +27,5 @@ function copy(t::FusedComputeTask{T1, T2}) where {T1, T2}
     )
 end
 
-FusedComputeTask{T1, T2}(
-    t1_inputs::Vector{String},
-    t1_output::String,
-    t2_inputs::Vector{String},
-) where {T1, T2} =
+FusedComputeTask{T1, T2}(t1_inputs::Vector{String}, t1_output::String, t2_inputs::Vector{String}) where {T1, T2} =
     FusedComputeTask{T1, T2}(T1(), T2(), t1_inputs, t1_output, t2_inputs)
diff --git a/src/task/type.jl b/src/task/type.jl
index 3b922c0..1bde12c 100644
--- a/src/task/type.jl
+++ b/src/task/type.jl
@@ -26,8 +26,7 @@ A fused compute task made up of the computation of first `T1` and then `T2`.
 
 Also see: [`get_types`](@ref).
 """
-struct FusedComputeTask{T1 <: AbstractComputeTask, T2 <: AbstractComputeTask} <:
-       AbstractComputeTask
+struct FusedComputeTask{T1 <: AbstractComputeTask, T2 <: AbstractComputeTask} <: AbstractComputeTask
     first_task::T1
     second_task::T2
     # the names of the inputs for T1
diff --git a/test/known_graphs.jl b/test/known_graphs.jl
index de81c12..6afbf9f 100644
--- a/test/known_graphs.jl
+++ b/test/known_graphs.jl
@@ -2,7 +2,7 @@ using Random
 
 function test_known_graph(name::String, n, fusion_test = true)
     @testset "Test $name Graph ($n)" begin
-        graph = parse_abc(joinpath(@__DIR__, "..", "input", "$name.txt"))
+        graph = parse_dag(joinpath(@__DIR__, "..", "input", "$name.txt"), ABCModel())
         props = get_properties(graph)
 
         if (fusion_test)
diff --git a/test/unit_tests_execution.jl b/test/unit_tests_execution.jl
index 71ebeaf..9fa5cd5 100644
--- a/test/unit_tests_execution.jl
+++ b/test/unit_tests_execution.jl
@@ -1,93 +1,73 @@
-import MetagraphOptimization.A
-import MetagraphOptimization.B
-import MetagraphOptimization.ParticleType
+import MetagraphOptimization.ABCParticle
 
 using QEDbase
 
 include("../examples/profiling_utilities.jl")
 
 @testset "Unit Tests Execution" begin
-    particles_2_2 = Tuple{Vector{Particle}, Vector{Particle}}((
-        [
-            Particle(SFourMomentum(0.823648, 0.0, 0.0, 0.823648), A),
-            Particle(SFourMomentum(0.823648, 0.0, 0.0, -0.823648), B),
-        ],
-        [
-            Particle(
-                SFourMomentum(0.823648, -0.835061, -0.474802, 0.277915),
-                A,
-            ),
-            Particle(SFourMomentum(0.823648, 0.835061, 0.474802, -0.277915), B),
-        ],
-    ))
+    process_2_2 = ABCProcessDescription(
+        Dict{Type, Int64}(ParticleA => 1, ParticleB => 1),
+        Dict{Type, Int64}(ParticleA => 1, ParticleB => 1),
+    )
 
-    expected_result = 5.5320505145845555e-5
+    particles_2_2 = ABCProcessInput(
+        process_2_2,
+        ABCParticle[
+            ParticleA(SFourMomentum(0.823648, 0.0, 0.0, 0.823648)),
+            ParticleB(SFourMomentum(0.823648, 0.0, 0.0, -0.823648)),
+        ],
+        ABCParticle[
+            ParticleA(SFourMomentum(0.823648, -0.835061, -0.474802, 0.277915)),
+            ParticleB(SFourMomentum(0.823648, 0.835061, 0.474802, -0.277915)),
+        ],
+    )
+    expected_result = 0.00013916495566048735
 
     @testset "AB->AB no optimization" begin
         for _ in 1:10   # test in a loop because graph layout should not change the result
-            graph = parse_abc(joinpath(@__DIR__, "..", "input", "AB->AB.txt"))
-            @test isapprox(
-                execute(graph, particles_2_2),
-                expected_result;
-                rtol = 0.001,
-            )
+            graph = parse_dag(joinpath(@__DIR__, "..", "input", "AB->AB.txt"), ABCModel())
+            @test isapprox(execute(graph, process_2_2, particles_2_2), expected_result; rtol = 0.001)
 
-            #=code = MetagraphOptimization.gen_code(graph)
-            @test isapprox(
-                execute(code, particles_2_2),
-                expected_result;
-                rtol = 0.001,
-            )=#
+            func = get_compute_function(graph, process_2_2)
+            @test isapprox(func(particles_2_2), expected_result; rtol = 0.001)
         end
     end
 
     @testset "AB->AB after random walk" begin
         for i in 1:100
-            graph = parse_abc(joinpath(@__DIR__, "..", "input", "AB->AB.txt"))
+            graph = parse_dag(joinpath(@__DIR__, "..", "input", "AB->AB.txt"), ABCModel())
             random_walk!(graph, 50)
             @test is_valid(graph)
 
-            @test isapprox(
-                execute(graph, particles_2_2),
-                expected_result;
-                rtol = 0.001,
-            )
+            @test isapprox(execute(graph, process_2_2, particles_2_2), expected_result; rtol = 0.001)
         end
     end
 
-    particles_2_4 = gen_particles([A, B], [A, B, B, B])
-    graph = parse_abc(joinpath(@__DIR__, "..", "input", "AB->ABBB.txt"))
-    expected_result = execute(graph, particles_2_4)
+    process_2_4 = ABCProcessDescription(
+        Dict{Type, Int64}(ParticleA => 1, ParticleB => 1),
+        Dict{Type, Int64}(ParticleA => 1, ParticleB => 3),
+    )
+    particles_2_4 = gen_process_input(process_2_4)
+    graph = parse_dag(joinpath(@__DIR__, "..", "input", "AB->ABBB.txt"), ABCModel())
+    expected_result = execute(graph, process_2_4, particles_2_4)
 
     @testset "AB->ABBB no optimization" begin
         for _ in 1:5   # test in a loop because graph layout should not change the result
-            graph = parse_abc(joinpath(@__DIR__, "..", "input", "AB->ABBB.txt"))
-            @test isapprox(
-                execute(graph, particles_2_4),
-                expected_result;
-                rtol = 0.001,
-            )
+            graph = parse_dag(joinpath(@__DIR__, "..", "input", "AB->ABBB.txt"), ABCModel())
+            @test isapprox(execute(graph, process_2_4, particles_2_4), expected_result; rtol = 0.001)
 
-            #=code = MetagraphOptimization.gen_code(graph)
-            @test isapprox(
-                execute(code, particles_2_4),
-                expected_result;
-                rtol = 0.001,
-            )=#
+            func = get_compute_function(graph, process_2_4)
+            @test isapprox(func(particles_2_4), expected_result; rtol = 0.001)
         end
     end
 
     @testset "AB->ABBB after random walk" begin
         for i in 1:20
-            graph = parse_abc(joinpath(@__DIR__, "..", "input", "AB->ABBB.txt"))
+            graph = parse_dag(joinpath(@__DIR__, "..", "input", "AB->ABBB.txt"), ABCModel())
             random_walk!(graph, 100)
             @test is_valid(graph)
 
-            @test isapprox(
-                execute(graph, particles_2_4),
-                expected_result;
-                rtol = 0.001,
-            )
+            @test isapprox(execute(graph, process_2_4, particles_2_4), expected_result; rtol = 0.001)
         end
     end
 
diff --git a/test/unit_tests_graph.jl b/test/unit_tests_graph.jl
index cab3305..bab3155 100644
--- a/test/unit_tests_graph.jl
+++ b/test/unit_tests_graph.jl
@@ -11,10 +11,8 @@ import MetagraphOptimization.partners
     @test length(graph.appliedOperations) == 0
     @test length(graph.operationsToApply) == 0
     @test length(graph.dirtyNodes) == 0
-    @test length(graph.diff) ==
-          (addedNodes = 0, removedNodes = 0, addedEdges = 0, removedEdges = 0)
-    @test length(get_operations(graph)) ==
-          (nodeFusions = 0, nodeReductions = 0, nodeSplits = 0)
+    @test length(graph.diff) == (addedNodes = 0, removedNodes = 0, addedEdges = 0, removedEdges = 0)
+    @test length(get_operations(graph)) == (nodeFusions = 0, nodeReductions = 0, nodeSplits = 0)
 
     # s to output (exit node)
     d_exit = insert_node!(graph, make_node(DataTask(10)), track = false)
@@ -107,8 +105,7 @@ import MetagraphOptimization.partners
     @test length(graph.appliedOperations) == 0
     @test length(graph.operationsToApply) == 0
     @test length(graph.dirtyNodes) == 26
-    @test length(graph.diff) ==
-          (addedNodes = 0, removedNodes = 0, addedEdges = 0, removedEdges = 0)
+    @test length(graph.diff) == (addedNodes = 0, removedNodes = 0, addedEdges = 0, removedEdges = 0)
 
     @test is_valid(graph)
 
@@ -135,8 +132,7 @@ import MetagraphOptimization.partners
     @test length(siblings(s0)) == 1
 
     operations = get_operations(graph)
-    @test length(operations) ==
-          (nodeFusions = 10, nodeReductions = 0, nodeSplits = 0)
+    @test length(operations) == (nodeFusions = 10, nodeReductions = 0, nodeSplits = 0)
     @test length(graph.dirtyNodes) == 0
 
     @test operations == get_operations(graph)
@@ -157,8 +153,7 @@ import MetagraphOptimization.partners
     @test length(graph.operationsToApply) == 1
     @test first(graph.operationsToApply) == nf
     @test length(graph.dirtyNodes) == 0
-    @test length(graph.diff) ==
-          (addedNodes = 0, removedNodes = 0, addedEdges = 0, removedEdges = 0)
+    @test length(graph.diff) == (addedNodes = 0, removedNodes = 0, addedEdges = 0, removedEdges = 0)
 
     # this applies pending operations
     properties = get_properties(graph)
@@ -176,8 +171,7 @@ import MetagraphOptimization.partners
     operations = get_operations(graph)
     @test length(graph.dirtyNodes) == 0
 
-    @test length(operations) ==
-          (nodeFusions = 9, nodeReductions = 0, nodeSplits = 0)
+    @test length(operations) == (nodeFusions = 9, nodeReductions = 0, nodeSplits = 0)
     @test !isempty(operations)
 
     possibleNF = 9
@@ -185,14 +179,12 @@ import MetagraphOptimization.partners
         push_operation!(graph, first(operations.nodeFusions))
         operations = get_operations(graph)
         possibleNF = possibleNF - 1
-        @test length(operations) ==
-              (nodeFusions = possibleNF, nodeReductions = 0, nodeSplits = 0)
+        @test length(operations) == (nodeFusions = possibleNF, nodeReductions = 0, nodeSplits = 0)
     end
 
     @test isempty(operations)
 
-    @test length(operations) ==
-          (nodeFusions = 0, nodeReductions = 0, nodeSplits = 0)
+    @test length(operations) == (nodeFusions = 0, nodeReductions = 0, nodeSplits = 0)
     @test length(graph.dirtyNodes) == 0
     @test length(graph.nodes) == 6
     @test length(graph.appliedOperations) == 10
@@ -213,8 +205,7 @@ import MetagraphOptimization.partners
     @test properties.computeIntensity ≈ 28 / 62
 
     operations = get_operations(graph)
-    @test length(operations) ==
-          (nodeFusions = 10, nodeReductions = 0, nodeSplits = 0)
+    @test length(operations) == (nodeFusions = 10, nodeReductions = 0, nodeSplits = 0)
 
     @test is_valid(graph)
 end
diff --git a/test/unit_tests_nodes.jl b/test/unit_tests_nodes.jl
index 74be0e8..7a274d0 100644
--- a/test/unit_tests_nodes.jl
+++ b/test/unit_tests_nodes.jl
@@ -3,8 +3,7 @@
     nC1 = MetagraphOptimization.make_node(MetagraphOptimization.ComputeTaskU())
     nC2 = MetagraphOptimization.make_node(MetagraphOptimization.ComputeTaskV())
     nC3 = MetagraphOptimization.make_node(MetagraphOptimization.ComputeTaskP())
-    nC4 =
-        MetagraphOptimization.make_node(MetagraphOptimization.ComputeTaskSum())
+    nC4 = MetagraphOptimization.make_node(MetagraphOptimization.ComputeTaskSum())
 
     nD1 = MetagraphOptimization.make_node(MetagraphOptimization.DataTask(10))
     nD2 = MetagraphOptimization.make_node(MetagraphOptimization.DataTask(20))
diff --git a/test/unit_tests_utility.jl b/test/unit_tests_utility.jl
index 169023e..db04d80 100644
--- a/test/unit_tests_utility.jl
+++ b/test/unit_tests_utility.jl
@@ -5,9 +5,7 @@
     @test MetagraphOptimization.bytes_to_human_readable(1025) == "1.001 KiB"
     @test MetagraphOptimization.bytes_to_human_readable(684235) == "668.2 KiB"
     @test MetagraphOptimization.bytes_to_human_readable(86214576) == "82.22 MiB"
-    @test MetagraphOptimization.bytes_to_human_readable(9241457698) ==
-          "8.607 GiB"
-    @test MetagraphOptimization.bytes_to_human_readable(3218598654367) ==
-          "2.927 TiB"
+    @test MetagraphOptimization.bytes_to_human_readable(9241457698) == "8.607 GiB"
+    @test MetagraphOptimization.bytes_to_human_readable(3218598654367) == "2.927 TiB"
 end
 println("Utility Unit Tests Complete!")