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.

// request.cf · coarse context

A page that knows where it met you.

Only coarse request metadata is shown. This demo does not display or persist visitor IP addresses.

Country
US
Cloudflare location
CMH
Connection
HTTP/2
Language
Not provided

Ray ID: a3ff157d9b963564

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