Edge Rewrite
// HTMLRewriter · presentation

This page was redesigned at the edge.

Cloudflare fetched the original article and streamed it through HTMLRewriter to apply an entirely new visual system without rebuilding the source page.

Jump to content

Module:SMILES2IUPAC/Enynes

From Wikipedia, the free encyclopedia
-- 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