Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Finish balancing PR #667

Merged
merged 22 commits into from
Sep 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/Catalyst.jl
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ include("graphs.jl")
export Graph, savegraph, complexgraph

# for creating compounds
include("compound.jl")
include("chemistry_functionality.jl")
export @compound
export components, iscompound, coefficients
export balance_reaction

end # module
197 changes: 197 additions & 0 deletions src/chemistry_functionality.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
struct CompoundSpecies end
struct CompoundComponents end
struct CompoundCoefficients end

Symbolics.option_to_metadata_type(::Val{:iscompound}) = CompoundSpecies
Symbolics.option_to_metadata_type(::Val{:components}) = CompoundComponents
Symbolics.option_to_metadata_type(::Val{:coefficients}) = CompoundCoefficients

macro compound(species_expr, arr_expr...)
# Ensure the species name is a valid expression
if !(species_expr isa Expr && species_expr.head == :call)
error("Invalid species name in @compound macro")
end

# Parse the species name to extract the species name and argument
species_name = species_expr.args[1]
species_arg = species_expr.args[2]

# Construct the expressions that define the species
species_expr = Expr(:macrocall, Symbol("@species"), LineNumberNode(0),
Expr(:call, species_name, species_arg))

# Construct the expression to set the iscompound metadata
setmetadata_expr = :($(species_name) = ModelingToolkit.setmetadata($(species_name),
Catalyst.CompoundSpecies,
true))

# Ensure the expressions are evaluated in the correct scope by escaping them
escaped_species_expr = esc(species_expr)
escaped_setmetadata_expr = esc(setmetadata_expr)

# Construct the array from the remaining arguments
arr = Expr(:vect, (arr_expr)...)
coeffs = []
species = []

for expr in arr_expr
if isa(expr, Expr) && expr.head == :call && expr.args[1] == :*
push!(coeffs, expr.args[2])
push!(species, expr.args[3])
else
push!(coeffs, 1)
push!(species, expr)
end
end

coeffs_expr = Expr(:vect, coeffs...)
species_expr = Expr(:vect, species...)

# Construct the expression to set the components metadata
setcomponents_expr = :($(species_name) = ModelingToolkit.setmetadata($(species_name),
Catalyst.CompoundComponents,
$species_expr))

# Ensure the expression is evaluated in the correct scope by escaping it
escaped_setcomponents_expr = esc(setcomponents_expr)

# Construct the expression to set the coefficients metadata
setcoefficients_expr = :($(species_name) = ModelingToolkit.setmetadata($(species_name),
Catalyst.CompoundCoefficients,
$coeffs_expr))

escaped_setcoefficients_expr = esc(setcoefficients_expr)

# Return a block that contains the escaped expressions
return Expr(:block, escaped_species_expr, escaped_setmetadata_expr,
escaped_setcomponents_expr, escaped_setcoefficients_expr)
end

# Check if a species is a compound
iscompound(s::Num) = iscompound(MT.value(s))
function iscompound(s)
MT.getmetadata(s, CompoundSpecies, false)
end

coefficients(s::Num) = coefficients(MT.value(s))
function coefficients(s)
MT.getmetadata(s, CompoundCoefficients)
end

components(s::Num) = components(MT.value(s))
function components(s)
MT.getmetadata(s, CompoundComponents)
end

component_coefficients(s::Num) = component_coefficients(MT.value(s))
function component_coefficients(s)
return [c => co for (c, co) in zip(components(s), coefficients(s))]
end


### Balancing Code
const COMPOUND_OF_COMPOUND_ERROR = ErrorException("Reaction balancing does not currently work for reactions involving compounds of compounds.")

# note this does not correctly handle compounds of compounds currently
function create_matrix(reaction::Catalyst.Reaction)
@unpack substrates, products = reaction
unique_atoms = [] # Array to store unique atoms
n_atoms = 0
ncompounds = length(substrates) + length(products)
A = zeros(Int, 0, ncompounds)

coeffsign = 1
jbase = 0
for compounds in (substrates, products)
for (j2, compound) in enumerate(compounds)
j = jbase + j2

if iscompound(compound)
atoms = components(compound)
any(iscompound, atoms) && throw(COMPOUND_OF_COMPOUND_ERROR)
coeffs = coefficients(compound)
(atoms == nothing || coeffs == nothing) && continue
else
# If not a compound, assume coefficient of 1
atoms = [compound]
coeffs = [1]
end

for (atom,coeff) in zip(atoms, coeffs)
# Extract atom and coefficient from the pair
i = findfirst(x -> isequal(x, atom), unique_atoms)
if i === nothing
# Add the atom to the atoms array if it's not already present
push!(unique_atoms, atom)
n_atoms += 1
A = [A; zeros(Int, 1, ncompounds)]
i = n_atoms
end

# Adjust coefficient based on whether the compound is a product or substrate
coeff *= coeffsign

A[i, j] = coeff
end
end

# update for iterating through products
coeffsign = -1
jbase = length(substrates)
end

return A
end

function get_balanced_stoich(reaction::Reaction)
# Create the reaction matrix A that is m atoms by n compounds
A = create_matrix(reaction)

# get an integer nullspace basis
X = ModelingToolkit.nullspace(A)
nullity = size(X, 2)

stoichvecs = Vector{Vector{Int64}}()
for (j, col) in enumerate(eachcol(X))
signs = unique(col)

# If there is only one basis vector and the signs are not all the same this means
# we have a solution that would require moving at least one substrate to be a
# product (or vice-versa). We therefore do not return anything in this case.
# If there are multiple basis vectors we don't currently determine if we can
# construct a linear combination giving a solution, so we just return them.
if (nullity > 1) || (all(>=(0), signs) || all(<=(0), signs))
coefs = abs.(col)
common_divisor = reduce(gcd, coefs)
coefs .= div.(coefs, common_divisor)
push!(stoichvecs, coefs)
end
end

return stoichvecs
end

function balance_reaction(reaction::Reaction)
# Calculate the stoichiometric coefficients for the balanced reaction.
stoichiometries = get_balanced_stoich(reaction)

balancedrxs = Vector{Reaction}(undef, length(stoichiometries))

# Iterate over each stoichiometry vector and create a reaction
for (i,stoich) in enumerate(stoichiometries)
# Divide the stoichiometry vector into substrate and product stoichiometries.
substoich = stoich[1:length(reaction.substrates)]
prodstoich = stoich[(length(reaction.substrates) + 1):end]

# Create a new reaction with the balanced stoichiometries
balancedrx = Reaction(reaction.rate, reaction.substrates,
reaction.products, substoich, prodstoich)

# Add the reaction to the vector of all reactions
balancedrxs[i] = balancedrx
end

isempty(balancedrxs) && (@warn "Unable to balance reaction.")
(length(balancedrxs) > 1) && (@warn "Infinite balanced reactions from ($reaction) are possible, returning a basis for them. Note that we do not check if they preserve the set of substrates and products from the original reaction.")
return balancedrxs
end
89 changes: 0 additions & 89 deletions src/compound.jl

This file was deleted.

1 change: 1 addition & 0 deletions test/miscellaneous_tests/compound_macro.jl
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,4 @@ let
@test isequal(components(A2),components(B2))
@test isequal(coefficients(A2), coefficients(B2))
end

Loading
Loading