Module:SMILES2IUPAC/Enynes
Appearance
-- Module:SMILES2IUPAC/Enynes
--
-- Naming engine for acyclic carbon compounds containing
-- C=C and/or C#C bonds.
--
-- Examples:
-- C=CC#C -> but-1-en-3-yne
-- C#CC#C -> buta-1,3-diyne
-- C=CC=CC#C -> penta-1,3-dien-5-yne
-- C=CC(C)=CC#C -> 3-methylhexa-1,3-dien-5-yne
local Data = require('Module:SMILES2IUPAC/Data')
local p = {}
local ROOT = Data.chainRoot
local MULTIPLIER = {
[1] = '',
[2] = 'di',
[3] = 'tri',
[4] = 'tetra',
[5] = 'penta',
[6] = 'hexa',
[7] = 'hepta',
[8] = 'octa',
[9] = 'nona',
[10] = 'deca'
}
local ALKYL = {
[1] = 'methyl',
[2] = 'ethyl',
[3] = 'propyl',
[4] = 'butyl',
[5] = 'pentyl',
[6] = 'hexyl',
[7] = 'heptyl',
[8] = 'octyl',
[9] = 'nonyl',
[10] = 'decyl'
}
----------------------------------------------------------------------
-- Copy a path
----------------------------------------------------------------------
local function copyPath(path)
local result = {}
for i = 1, #path do
result[i] = path[i]
end
return result
end
----------------------------------------------------------------------
-- Reverse a path
----------------------------------------------------------------------
local function reversePath(path)
local result = {}
for i = #path, 1, -1 do
result[#result + 1] = path[i]
end
return result
end
----------------------------------------------------------------------
-- Build adjacency list
----------------------------------------------------------------------
local function buildAdjacency(graph)
local adj = {}
for i = 1, #graph.atoms do
adj[i] = {}
end
for _, bond in ipairs(graph.bonds) do
adj[bond.a][#adj[bond.a] + 1] = {
to = bond.b,
order = bond.order
}
adj[bond.b][#adj[bond.b] + 1] = {
to = bond.a,
order = bond.order
}
end
return adj
end
----------------------------------------------------------------------
-- Get bond between two atoms
----------------------------------------------------------------------
local function getBond(graph, a, b)
for _, bond in ipairs(graph.bonds) do
if
(bond.a == a and bond.b == b)
or
(bond.a == b and bond.b == a)
then
return bond
end
end
return nil
end
----------------------------------------------------------------------
-- Count multiple bonds
----------------------------------------------------------------------
local function countMultipleBonds(graph)
local doubles = 0
local triples = 0
for _, bond in ipairs(graph.bonds) do
if bond.order == 2 then
doubles = doubles + 1
elseif bond.order == 3 then
triples = triples + 1
end
end
return doubles, triples
end
----------------------------------------------------------------------
-- Check that all atoms are carbon
----------------------------------------------------------------------
local function validateAtoms(graph)
for _, atom in ipairs(graph.atoms) do
if atom.element ~= 'C' then
return false,
'only carbon hydrocarbons are supported'
end
end
return true
end
----------------------------------------------------------------------
-- Ring detection
----------------------------------------------------------------------
local function isAcyclic(graph)
local adj = buildAdjacency(graph)
local visited = {}
local function dfs(node, parent)
visited[node] = true
for _, edge in ipairs(adj[node]) do
if not visited[edge.to] then
if dfs(edge.to, node) then
return true
end
elseif edge.to ~= parent then
return true
end
end
return false
end
for i = 1, #graph.atoms do
if not visited[i] then
if dfs(i, nil) then
return false
end
end
end
return true
end
----------------------------------------------------------------------
-- Lexicographical comparison of locant lists
----------------------------------------------------------------------
local function compareLocantLists(a, b)
if not b then
return true
end
for i = 1, math.min(#a, #b) do
if a[i] ~= b[i] then
return a[i] < b[i]
end
end
if #a ~= #b then
return #a < #b
end
return false
end
----------------------------------------------------------------------
-- Make combined multiple-bond locant list
----------------------------------------------------------------------
local function combinedLocants(candidate)
local result = {}
for _, x in ipairs(candidate.doubles) do
result[#result + 1] = x
end
for _, x in ipairs(candidate.triples) do
result[#result + 1] = x
end
table.sort(result)
return result
end
----------------------------------------------------------------------
-- Count branches attached to a candidate
----------------------------------------------------------------------
local function countBranches(graph, path)
local parent = {}
for _, atom in ipairs(path) do
parent[atom] = true
end
local adj = buildAdjacency(graph)
local seen = {}
local count = 0
for _, atom in ipairs(path) do
for _, edge in ipairs(adj[atom]) do
if not parent[edge.to]
and not seen[edge.to]
then
seen[edge.to] = true
count = count + 1
end
end
end
return count
end
----------------------------------------------------------------------
-- Build candidate
----------------------------------------------------------------------
local function makeCandidate(graph, path)
local doubles = {}
local triples = {}
for i = 1, #path - 1 do
local bond =
getBond(
graph,
path[i],
path[i + 1]
)
if bond then
if bond.order == 2 then
doubles[#doubles + 1] = i
elseif bond.order == 3 then
triples[#triples + 1] = i
end
end
end
--------------------------------------------------------------
-- CRITICAL:
-- DFS mutates its path table. Therefore the candidate must
-- receive its own copy.
--------------------------------------------------------------
local pathCopy =
copyPath(path)
return {
path = pathCopy,
length = #pathCopy,
doubleCount = #doubles,
tripleCount = #triples,
doubles = doubles,
triples = triples,
branchCount =
countBranches(
graph,
pathCopy
)
}
end
----------------------------------------------------------------------
-- Candidate ranking
--
-- 1. Maximum number of multiple bonds
-- 2. Maximum parent-chain length
-- 3. Lowest combined multiple-bond locants
-- 4. Lowest double-bond locants
-- 5. Lowest triple-bond locants
-- 6. Maximum substituent count
----------------------------------------------------------------------
local function candidateIsBetter(a, b)
if not b then
return true
end
--------------------------------------------------------------
-- 1. Maximum number of multiple bonds
--------------------------------------------------------------
local aMultiple =
a.doubleCount + a.tripleCount
local bMultiple =
b.doubleCount + b.tripleCount
if aMultiple ~= bMultiple then
return aMultiple > bMultiple
end
--------------------------------------------------------------
-- 2. Maximum chain length
--------------------------------------------------------------
if a.length ~= b.length then
return a.length > b.length
end
--------------------------------------------------------------
-- 3. Lowest combined multiple-bond locants
--------------------------------------------------------------
local aAll =
combinedLocants(a)
local bAll =
combinedLocants(b)
local aBetter =
compareLocantLists(
aAll,
bAll
)
local bBetter =
compareLocantLists(
bAll,
aAll
)
if aBetter ~= bBetter then
return aBetter
end
--------------------------------------------------------------
-- 4. Lowest double-bond locants
--------------------------------------------------------------
aBetter =
compareLocantLists(
a.doubles,
b.doubles
)
bBetter =
compareLocantLists(
b.doubles,
a.doubles
)
if aBetter ~= bBetter then
return aBetter
end
--------------------------------------------------------------
-- 5. Lowest triple-bond locants
--------------------------------------------------------------
aBetter =
compareLocantLists(
a.triples,
b.triples
)
bBetter =
compareLocantLists(
b.triples,
a.triples
)
if aBetter ~= bBetter then
return aBetter
end
--------------------------------------------------------------
-- 6. Maximum branches
--------------------------------------------------------------
if a.branchCount ~= b.branchCount then
return a.branchCount >
b.branchCount
end
return false
end
----------------------------------------------------------------------
-- Find best parent chain
----------------------------------------------------------------------
local function findParentChain(
graph,
doubleCount,
tripleCount
)
local adj =
buildAdjacency(graph)
local best = nil
local function dfs(
current,
visited,
path
)
visited[current] = true
path[#path + 1] = current
----------------------------------------------------------
-- Evaluate current path
----------------------------------------------------------
if #path >= 2 then
local candidate =
makeCandidate(
graph,
path
)
------------------------------------------------------
-- It must contain ALL multiple bonds.
------------------------------------------------------
if candidate.doubleCount == doubleCount
and candidate.tripleCount == tripleCount
then
--------------------------------------------------
-- Test reverse numbering.
--------------------------------------------------
local reversed =
reversePath(path)
local reverseCandidate =
makeCandidate(
graph,
reversed
)
if candidateIsBetter(
reverseCandidate,
candidate
) then
candidate =
reverseCandidate
end
--------------------------------------------------
-- Compare globally.
--------------------------------------------------
if candidateIsBetter(
candidate,
best
) then
best = candidate
end
end
end
----------------------------------------------------------
-- Continue DFS
----------------------------------------------------------
for _, edge in ipairs(adj[current]) do
if not visited[edge.to] then
dfs(
edge.to,
visited,
path
)
end
end
path[#path] = nil
visited[current] = nil
end
--------------------------------------------------------------
-- Start DFS from every atom
--------------------------------------------------------------
for start = 1, #graph.atoms do
dfs(
start,
{},
{}
)
end
if not best then
return nil
end
return best.path
end
----------------------------------------------------------------------
-- Get bond locants for a chain
----------------------------------------------------------------------
local function getBondLocants(
graph,
chain
)
local doubles = {}
local triples = {}
for i = 1, #chain - 1 do
local bond =
getBond(
graph,
chain[i],
chain[i + 1]
)
if bond then
if bond.order == 2 then
doubles[#doubles + 1] = i
elseif bond.order == 3 then
triples[#triples + 1] = i
end
end
end
return doubles, triples
end
----------------------------------------------------------------------
-- Choose best numbering direction
----------------------------------------------------------------------
local function chooseDirection(
graph,
chain
)
local fd, ft =
getBondLocants(
graph,
chain
)
local reversed =
reversePath(chain)
local rd, rt =
getBondLocants(
graph,
reversed
)
local forward = {
path = chain,
length = #chain,
doubleCount = #fd,
tripleCount = #ft,
doubles = fd,
triples = ft,
branchCount =
countBranches(
graph,
chain
)
}
local reverse = {
path = reversed,
length = #reversed,
doubleCount = #rd,
tripleCount = #rt,
doubles = rd,
triples = rt,
branchCount =
countBranches(
graph,
reversed
)
}
if candidateIsBetter(
reverse,
forward
) then
return reversed, rd, rt
end
return chain, fd, ft
end
----------------------------------------------------------------------
-- Find a simple alkyl substituent
----------------------------------------------------------------------
local function findSubstituent(
graph,
adj,
parentSet,
start
)
local visited = {}
local count = 0
local function walk(node)
visited[node] = true
count = count + 1
for _, edge in ipairs(adj[node]) do
local nextAtom = edge.to
if not parentSet[nextAtom]
and not visited[nextAtom]
then
if edge.order ~= 1 then
return false
end
if not walk(nextAtom) then
return false
end
end
end
return true
end
if not walk(start) then
return nil
end
return {
size = count,
root = ALKYL[count]
}
end
----------------------------------------------------------------------
-- Collect substituents
----------------------------------------------------------------------
local function collectSubstituents(
graph,
chain
)
local adj =
buildAdjacency(graph)
local parentSet = {}
for _, atom in ipairs(chain) do
parentSet[atom] = true
end
local substituents = {}
local processed = {}
for position, atom in ipairs(chain) do
for _, edge in ipairs(adj[atom]) do
local branch = edge.to
if not parentSet[branch]
and not processed[branch]
then
processed[branch] = true
if edge.order ~= 1 then
return nil,
'unsaturated substituent is not supported yet'
end
local sub =
findSubstituent(
graph,
adj,
parentSet,
branch
)
if not sub then
return nil,
'complex substituent is not supported yet'
end
if not sub.root then
return nil,
'substituent is too large'
end
sub.locant = position
substituents[
#substituents + 1
] = sub
end
end
end
return substituents
end
----------------------------------------------------------------------
-- Build substituent prefix
----------------------------------------------------------------------
local function makeSubstituentPrefix(
substituents
)
if #substituents == 0 then
return ''
end
local groups = {}
for _, sub in ipairs(substituents) do
if not groups[sub.root] then
groups[sub.root] = {
root = sub.root,
locants = {}
}
end
groups[sub.root].locants[
#groups[sub.root].locants + 1
] = sub.locant
end
local ordered = {}
for _, group in pairs(groups) do
ordered[#ordered + 1] = group
end
table.sort(
ordered,
function(a, b)
return a.root < b.root
end
)
local parts = {}
for _, group in ipairs(ordered) do
table.sort(
group.locants,
function(a, b)
return a < b
end
)
local locants = {}
for _, locant in ipairs(group.locants) do
locants[#locants + 1] =
tostring(locant)
end
local multiplier =
MULTIPLIER[
#group.locants
] or ''
parts[#parts + 1] =
table.concat(
locants,
','
)
.. '-'
.. multiplier
.. group.root
end
return table.concat(
parts,
'-'
)
end
----------------------------------------------------------------------
-- Build parent hydrocarbon name
----------------------------------------------------------------------
local function makeUnsaturationName(
chainLength,
doubles,
triples
)
local root =
ROOT[chainLength]
if not root then
return nil,
'no chain root available for '
.. tostring(chainLength)
.. ' carbons'
end
local doubleCount = #doubles
local tripleCount = #triples
if doubleCount == 0
and tripleCount == 0
then
return nil,
'no multiple bonds found'
end
local doubleLocants = {}
for _, x in ipairs(doubles) do
doubleLocants[#doubleLocants + 1] =
tostring(x)
end
local tripleLocants = {}
for _, x in ipairs(triples) do
tripleLocants[#tripleLocants + 1] =
tostring(x)
end
local multipleCount =
doubleCount + tripleCount
--------------------------------------------------------------
-- Insert "a" before en/yne when multiple bonds > 1.
--------------------------------------------------------------
local stem = root
if multipleCount > 1 then
stem = root .. 'a'
end
local parts = {}
--------------------------------------------------------------
-- Double bonds
--------------------------------------------------------------
if doubleCount > 0 then
local locants =
table.concat(
doubleLocants,
','
)
local multiplier =
MULTIPLIER[doubleCount]
if doubleCount == 1 then
if tripleCount > 0 then
parts[#parts + 1] =
locants .. '-en'
else
parts[#parts + 1] =
locants .. '-ene'
end
else
if tripleCount > 0 then
parts[#parts + 1] =
locants
.. '-'
.. multiplier
.. 'en'
else
parts[#parts + 1] =
locants
.. '-'
.. multiplier
.. 'ene'
end
end
end
--------------------------------------------------------------
-- Triple bonds
--------------------------------------------------------------
if tripleCount > 0 then
local locants =
table.concat(
tripleLocants,
','
)
local multiplier =
MULTIPLIER[tripleCount]
if tripleCount == 1 then
parts[#parts + 1] =
locants .. '-yne'
else
parts[#parts + 1] =
locants
.. '-'
.. multiplier
.. 'yne'
end
end
return stem
.. '-'
.. table.concat(
parts,
'-'
)
end
----------------------------------------------------------------------
-- Main naming function
----------------------------------------------------------------------
function p.name(graph)
if not graph
or not graph.atoms
or not graph.bonds
then
return nil,
'invalid graph'
end
--------------------------------------------------------------
-- Carbon only
--------------------------------------------------------------
local valid,
err =
validateAtoms(graph)
if not valid then
return nil, err
end
--------------------------------------------------------------
-- Multiple bonds
--------------------------------------------------------------
local doubleCount,
tripleCount =
countMultipleBonds(graph)
if doubleCount == 0
and tripleCount == 0
then
return nil,
'no multiple bonds found'
end
--------------------------------------------------------------
-- Rings are handled elsewhere
--------------------------------------------------------------
if not isAcyclic(graph) then
return nil,
'rings are not supported yet'
end
--------------------------------------------------------------
-- Parent chain
--------------------------------------------------------------
local chain =
findParentChain(
graph,
doubleCount,
tripleCount
)
if not chain then
return nil,
'could not find a parent chain containing all multiple bonds'
end
--------------------------------------------------------------
-- Numbering direction
--------------------------------------------------------------
local doubles
local triples
chain,
doubles,
triples =
chooseDirection(
graph,
chain
)
--------------------------------------------------------------
-- Substituents
--------------------------------------------------------------
local substituents,
subErr =
collectSubstituents(
graph,
chain
)
if not substituents then
return nil, subErr
end
--------------------------------------------------------------
-- Parent name
--------------------------------------------------------------
local parentName,
parentErr =
makeUnsaturationName(
#chain,
doubles,
triples
)
if not parentName then
return nil, parentErr
end
--------------------------------------------------------------
-- Prefix
--------------------------------------------------------------
local prefix =
makeSubstituentPrefix(
substituents
)
if prefix ~= '' then
return prefix
.. parentName
end
return parentName
end
----------------------------------------------------------------------
-- Debug function
----------------------------------------------------------------------
function p.debug(frame)
local Parser =
require('Module:SMILES2IUPAC/Parser')
local smiles =
frame.args[1]
or frame.args.smiles
local graph, err =
Parser.parse(smiles)
if not graph then
return 'Parser error: '
.. tostring(err)
end
local out = {}
local doubleCount,
tripleCount =
countMultipleBonds(graph)
out[#out + 1] =
'Double bonds: '
.. tostring(doubleCount)
out[#out + 1] =
'Triple bonds: '
.. tostring(tripleCount)
local chain =
findParentChain(
graph,
doubleCount,
tripleCount
)
if not chain then
out[#out + 1] =
'Parent chain: NONE'
return table.concat(
out,
'\n'
)
end
out[#out + 1] =
'Parent chain: '
.. table.concat(
chain,
'-'
)
local doubles,
triples =
getBondLocants(
graph,
chain
)
out[#out + 1] =
'Double locants: '
.. table.concat(
doubles,
','
)
out[#out + 1] =
'Triple locants: '
.. table.concat(
triples,
','
)
local subs =
collectSubstituents(
graph,
chain
)
if subs then
out[#out + 1] =
'Substituents: '
.. tostring(#subs)
else
out[#out + 1] =
'Substituents: ERROR'
end
return table.concat(
out,
'\n'
)
end
return p