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: a4114bf3d83e0abb

Jump to content

Module:SMILES2IUPAC/Chain

From Wikipedia, the free encyclopedia
-- Module:SMILES2IUPAC/Chain
--
-- Generic acyclic-tree algorithms used to select an IUPAC
-- parent chain and its substituents.
--
-- Supports:
--   * acyclic carbon trees
--   * longest-chain selection
--   * priority nodes
--   * substituent detection
--   * lowest-locant comparison

local p = {}

----------------------------------------------------------------------
-- Build adjacency list
----------------------------------------------------------------------

function p.buildAdjacency(nodeSet, edges)
	local adj = {}
	local inSet = {}

	for _, n in ipairs(nodeSet) do
		adj[n] = {}
		inSet[n] = true
	end

	for _, e in ipairs(edges) do
		if inSet[e.a] and inSet[e.b] then
			table.insert(adj[e.a], e.b)
			table.insert(adj[e.b], e.a)
		end
	end

	return adj
end

----------------------------------------------------------------------
-- Check whether graph is a connected tree
----------------------------------------------------------------------

function p.isTree(nodeSet, adj)
	if #nodeSet == 0 then
		return false, 'empty graph'
	end

	local edgeCount = 0

	for _, n in ipairs(nodeSet) do
		edgeCount = edgeCount + #(adj[n] or {})
	end

	edgeCount = edgeCount / 2

	if edgeCount ~= #nodeSet - 1 then
		return false,
			'not a tree (contains a ring, or is disconnected)'
	end

	local visited = {
		[nodeSet[1]] = true
	}

	local queue = {
		nodeSet[1]
	}

	local qi = 1
	local count = 1

	while qi <= #queue do
		local cur = queue[qi]
		qi = qi + 1

		for _, nb in ipairs(adj[cur] or {}) do
			if not visited[nb] then
				visited[nb] = true
				count = count + 1
				table.insert(queue, nb)
			end
		end
	end

	if count ~= #nodeSet then
		return false, 'disconnected graph'
	end

	return true
end

----------------------------------------------------------------------
-- Find leaves
----------------------------------------------------------------------

function p.leaves(nodeSet, adj)
	if #nodeSet == 1 then
		return {
			nodeSet[1]
		}
	end

	local result = {}

	for _, n in ipairs(nodeSet) do
		if #(adj[n] or {}) <= 1 then
			table.insert(result, n)
		end
	end

	return result
end

----------------------------------------------------------------------
-- Find path between two nodes
----------------------------------------------------------------------

function p.pathBetween(adj, startNode, endNode)
	if startNode == endNode then
		return {
			startNode
		}
	end

	local visited = {
		[startNode] = true
	}

	local parent = {}

	local queue = {
		startNode
	}

	local qi = 1

	while qi <= #queue do
		local cur = queue[qi]
		qi = qi + 1

		if cur == endNode then
			break
		end

		for _, nb in ipairs(adj[cur] or {}) do
			if not visited[nb] then
				visited[nb] = true
				parent[nb] = cur
				table.insert(queue, nb)
			end
		end
	end

	if not visited[endNode] then
		return nil
	end

	local path = {
		endNode
	}

	local cur = endNode

	while cur ~= startNode do
		cur = parent[cur]

		if not cur then
			return nil
		end

		table.insert(
			path,
			1,
			cur
		)
	end

	return path
end

----------------------------------------------------------------------
-- Find every longest leaf-to-leaf path
--
-- Important:
-- The parent chain must be the LONGEST possible carbon chain.
-- A shorter chain with more convenient substituents must never
-- replace a longer chain.
----------------------------------------------------------------------

function p.findLongestChains(nodeSet, adj)
	if #nodeSet == 1 then
		return {
			{
				nodeSet[1]
			}
		}
	end

	local leafNodes = p.leaves(
		nodeSet,
		adj
	)

	local bestLength = 0
	local candidates = {}

	for i = 1, #leafNodes do
		for j = i + 1, #leafNodes do

			local path = p.pathBetween(
				adj,
				leafNodes[i],
				leafNodes[j]
			)

			if path then
				local length = #path

				if length > bestLength then
					bestLength = length

					candidates = {
						path
					}

				elseif length == bestLength then
					table.insert(
						candidates,
						path
					)
				end
			end
		end
	end

	return candidates
end

----------------------------------------------------------------------
-- Check whether a path contains any priority node
----------------------------------------------------------------------

local function containsPriorityNodes(
	path,
	prioritySet
)
	if not prioritySet then
		return false
	end

	for _, node in ipairs(path) do
		if prioritySet[node] then
			return true
		end
	end

	return false
end

----------------------------------------------------------------------
-- Get all nodes belonging to a substituent subtree
----------------------------------------------------------------------

function p.substituentSubtree(
	adj,
	root,
	pathSet
)
	local visited = {}
	local result = {}

	local function dfs(node)
		if visited[node] then
			return
		end

		visited[node] = true

		if pathSet[node] then
			return
		end

		table.insert(
			result,
			node
		)

		for _, nb in ipairs(adj[node] or {}) do
			if not visited[nb]
				and not pathSet[nb]
			then
				dfs(nb)
			end
		end
	end

	dfs(root)

	return result
end

----------------------------------------------------------------------
-- Find branches attached to each parent-chain atom
----------------------------------------------------------------------

function p.branchesAlongPath(
	path,
	adj
)
	local pathSet = {}

	for _, n in ipairs(path) do
		pathSet[n] = true
	end

	local branches = {}

	for index, n in ipairs(path) do
		branches[index] = {}

		for _, nb in ipairs(adj[n] or {}) do
			if not pathSet[nb] then

				local nodes =
					p.substituentSubtree(
						adj,
						nb,
						pathSet
					)

				table.insert(
					branches[index],
					{
						root = nb,
						nodes = nodes
					}
				)
			end
		end
	end

	return branches
end

----------------------------------------------------------------------
-- Count substituent branches
----------------------------------------------------------------------

function p.countSubstituents(branches)
	local count = 0

	for _, list in pairs(branches) do
		count = count + #list
	end

	return count
end

----------------------------------------------------------------------
-- Get substituent locants for a numbering direction
----------------------------------------------------------------------

function p.locantsForDirection(
	path,
	branches,
	reversed
)
	local n = #path
	local locants = {}

	for index = 1, n do

		local locant

		if reversed then
			locant = n - index + 1
		else
			locant = index
		end

		for _ = 1, #(branches[index] or {}) do
			table.insert(
				locants,
				locant
			)
		end
	end

	table.sort(locants)

	return locants
end

----------------------------------------------------------------------
-- Compare two locant lists
--
-- Returns:
--   -1 if a is better
--    0 if equal
--    1 if b is better
----------------------------------------------------------------------

function p.compareLocantLists(a, b)
	local n = math.min(
		#a,
		#b
	)

	for i = 1, n do
		if a[i] ~= b[i] then

			if a[i] < b[i] then
				return -1
			else
				return 1
			end
		end
	end

	if #a ~= #b then
		if #a < #b then
			return -1
		else
			return 1
		end
	end

	return 0
end

----------------------------------------------------------------------
-- Determine whether forward numbering is better
----------------------------------------------------------------------

function p.forwardIsBetter(
	path,
	branches
)
	local forward =
		p.locantsForDirection(
			path,
			branches,
			false
		)

	local backward =
		p.locantsForDirection(
			path,
			branches,
			true
		)

	return p.compareLocantLists(
		forward,
		backward
	) <= 0
end

----------------------------------------------------------------------
-- Select the best parent chain
--
-- Priority:
--
-- 1. Longest chain
-- 2. Contains required functional-group/priority atom
-- 3. Greatest number of substituents
-- 4. Lowest locant set
--
-- Since findLongestChains() ONLY returns longest chains,
-- criterion #1 is guaranteed here.
----------------------------------------------------------------------

function p.chooseBestChain(
	nodeSet,
	adj,
	priorityNodes
)
	local candidates =
		p.findLongestChains(
			nodeSet,
			adj
		)

	if #candidates == 0 then
		return nil
	end

	------------------------------------------------------------------
	-- Functional-group priority
	------------------------------------------------------------------

	if priorityNodes
		and next(priorityNodes)
	then

		local filtered = {}

		for _, path in ipairs(candidates) do
			if containsPriorityNodes(
				path,
				priorityNodes
			) then

				table.insert(
					filtered,
					path
				)
			end
		end

		if #filtered > 0 then
			candidates = filtered
		end
	end

	------------------------------------------------------------------
	-- Compare longest-chain candidates
	------------------------------------------------------------------

	local best = nil
	local bestBranches = nil
	local bestLocants = nil
	local bestSubCount = -1

	for _, path in ipairs(candidates) do

		local branches =
			p.branchesAlongPath(
				path,
				adj
			)

		local subCount =
			p.countSubstituents(
				branches
			)

		local forward =
			p.locantsForDirection(
				path,
				branches,
				false
			)

		local backward =
			p.locantsForDirection(
				path,
				branches,
				true
			)

		local locants

		if p.compareLocantLists(
			forward,
			backward
		) <= 0 then
			locants = forward
		else
			locants = backward
		end

		local better = false

		if not best then
			better = true

		elseif subCount > bestSubCount then
			better = true

		elseif subCount == bestSubCount
			and p.compareLocantLists(
				locants,
				bestLocants
			) < 0
		then
			better = true
		end

		if better then
			best = path
			bestBranches = branches
			bestLocants = locants
			bestSubCount = subCount
		end
	end

	return best
end

return p