Classify chemicals into 119 PFAS structural groups
Server-side analysis powered by the Python PFASGroups package. Upload or paste SMILES; results include group membership and fluorinated component size.
OpenCheck against 5 major regulatory PFAS definitions
Tests each molecule against OECD (2021), EU Restriction, US EPA OPPT (2023), UK HSE, and PFASSTRUCTv5, with per-criterion explanations.
OpenFind non-PFAS alternatives for industrial applications
Browse the ZeroPM Alternatives Assessment Database by application, function, or hazard profile.
OpenDraw and validate chemical structures from SMILES
Instantly render any SMILES or SMARTS string as a 2D structure diagram using RDKit-WASM.
OpenContribute a PFAS use case or alternative
Fill in a structured form to submit a new PFAS application, function, or substitute compound for review.
OpenResolve CAS numbers and names to chemical structures
Look up SMILES, InChI, formula, and synonyms for any compound via the PubChem PUG REST API.
OpenGET /health
POST /analyze - Classify a single SMILES into PFAS groups
POST /analyze-batch - Classify multiple SMILES at once
GET /groups - List all group definitions
GET /groups/:id - Fetch one group by ID
POST /check-definitions - Test against 5 regulatory PFAS definitions
GET /definitions - List all regulatory definitions
GET /alternatives-db/pfas - PFAS uses (6,513 entries)
GET /alternatives-db/functions - PFAS functions (966 entries)
GET /alternatives-db/alternatives - Alternatives (3,389 entries)
GET /alternatives-db/all - All sheets combined
POST /alternatives-db/submit - Submit new entries for review
To ensure fair usage and server stability, the following rate limits apply per IP address:
Rate limit headers (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset) are included in all API responses.
import requests import json # Analyze a single PFAS molecule url = "https://chem.cogitopia.dev/analyze" payload = { "input": "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)S(=O)(=O)O", "inputType": "smiles" } response = requests.post(url, json=payload) result = response.json() print(f"Matches found: {len(result['matches'])}") for match in result['matches']: print(f" - {match['name']} ({','.join([m['SMARTS'] + ' size: ' + str(m['size']) for m in match['components']])})")
import requests # Single flat table — one row per group match per molecule SMILES = [ "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)S(=O)(=O)O", # PFOS "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(=O)O", # PFOA ] payload = {"molecules": [{"smiles": s} for s in SMILES]} results = requests.post("https://chem.cogitopia.dev/analyze-batch", json=payload).json() hdr = (f"{'mol':>3} {'SMILES':<20} {'Group':<35} {'type':<11} " f"{'#c':>2} {'maxSz':>5} {'sizes':<14} {'totC':>4}") print(hdr) print("-" * len(hdr)) for i, (smi, result) in enumerate(zip(SMILES, results['results']), 1): for m in result['matches']: comps = m.get('components', []) search = (m['name'] + " " + " ".join(c.get('SMARTS', '') for c in comps)).lower() typ = ("perfluoro" if "perfluoro" in search else "polyfluoro" if "polyfluoro" in search else "-") sizes = [c['size'] for c in comps] tot_c = sum(sizes) max_sz = max(sizes, default=0) print(f"{i:>3} {smi[:20]:<20} {m['name']:<35} {typ:<11} " f"{len(comps):>2} {max_sz:>5} {str(sizes):<14} {tot_c:>4}")
import requests # Fetch all PFAS groups response = requests.get("https://chem.cogitopia.dev/groups") groups = response.json() print(f"Total groups: {len(groups)}") # Get alternatives database response = requests.get("https://chem.cogitopia.dev/alternatives-db/all") db = response.json() print(f"PFAS uses: {len(db['pfas'])}") print(f"Functions: {len(db['functions'])}") print(f"Alternatives: {len(db['alternatives'])}")
import requests # Check molecules against all 5 PFAS regulatory definitions molecules = [ "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)S(=O)(=O)O", # PFOS "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(=O)O", # PFOA "CCO", # ethanol (non-PFAS) ] data = requests.post("https://chem.cogitopia.dev/check-definitions", json={"molecules": molecules}).json() defs = list(data["results"][0]["definitions"].keys()) col_w = [len(d.upper()) for d in defs] hdr = f"{'SMILES':<32} " + " ".join(d.upper() for d in defs) print(hdr) print("-" * len(hdr)) for r in data["results"]: flags = " ".join(("✓" if v["matched"] else "✗").center(w) for v, w in zip(r["definitions"].values(), col_w)) print(f"{r['smiles'][:32]:<32} {flags}") print(" Statistics:") for def_id, stats in data["statistics"].items(): print(f" {def_id}: {stats['matched']}/{stats['total']} matched")
import requests molecules = [ "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)S(=O)(=O)O", # PFOS "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(=O)O", # PFOA "CCO", # ethanol (non-PFAS) ] # score = a*sum(|CF|) + b*percentile(|CF|, p) — same formula as the web interface # defaults: a=1.0, b=5.0, p=90 (90th percentile), group=51 resp = requests.post("https://chem.cogitopia.dev/prioritize", json={"molecules": molecules}).json() print(f"{'#':>2} {'score':>7} {'max_sz':>6} SMILES") print("-" * 90) for r in resp["ranked"]: print(f"{r['rank']:>2} {r['score']:>7.1f} {r['max_size']:>6} {r['smiles']}") # Custom weights: emphasise chain length (b=10, p=100 = max component only) resp2 = requests.post("https://chem.cogitopia.dev/prioritize", json={"molecules": molecules, "b": 10, "p": 100}).json() print(" Custom (b=10, p=100):") for r in resp2["ranked"]: print(f"{r['rank']:>2} {r['score']:>7.1f} {r['smiles']}")
library(httr) # Analyze a single PFAS molecule url <- "https://chem.cogitopia.dev/analyze" payload <- list( input = "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)S(=O)(=O)O", inputType = "smiles" ) response <- POST(url, body = payload, encode = "json", content_type_json()) result <- content(response, "parsed") cat("Matches found:", length(result$matches), "\n") for (match in result$matches) { comp_str <- paste(sapply(match$components, function(c) paste0(c$SMARTS, " size: ", c$size)), collapse=",") cat(" -", match$name, "(", comp_str, ")\n") }
library(httr) # Single flat table — one row per group match per molecule SMILES <- c( "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)S(=O)(=O)O", # PFOS "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(=O)O" # PFOA ) response <- POST("https://chem.cogitopia.dev/analyze-batch", body = list(molecules = lapply(SMILES, function(s) list(smiles = s))), encode = "json") results <- content(response, "parsed") # Compute SMILES column width from actual data so header aligns with rows smi_w <- max(nchar(SMILES)) fmt <- paste0("%-3s %-", smi_w, "s %-35s %-11s %2s %5s %-12s %4s") rows <- character(0) records <- list() for (i in seq_along(results$results)) { smi <- SMILES[i] for (m in results$results[[i]]$matches) { comps <- m$components search <- tolower(paste(m$name, paste(sapply(comps, function(c) c$SMARTS), collapse = " "))) typ <- if (grepl("perfluoro", search)) "perfluoro" else if (grepl("polyfluoro", search)) "polyfluoro" else "-" sizes <- sapply(comps, function(c) c$size) tot_c <- sum(sizes) max_sz <- max(sizes) sz_str <- paste0("[", paste(sizes, collapse = ","), "]") rows <- c(rows, sprintf(fmt, i, smi, m$name, typ, length(comps), max_sz, sz_str, tot_c)) records[[length(records) + 1]] <- list( mol = i, SMILES = smi, Group = m$name, type = typ, nc = length(comps), maxSz = max_sz, sizes = sz_str, totC = tot_c) } } hdr <- sprintf(fmt, "mol", "SMILES", "Group", "type", "#c", "maxSz", "sizes", "totC") writeLines(c(hdr, strrep("-", nchar(hdr)), rows)) # Save as CSV write.csv(do.call(rbind, lapply(records, as.data.frame)), "batch_results.csv", row.names = FALSE) cat("Saved batch_results.csv ")
library(httr); library(jsonlite) # Check molecules against all 5 PFAS regulatory definitions molecules <- list( "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)S(=O)(=O)O", # PFOS "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(=O)O", # PFOA "CCO" # ethanol (non-PFAS) ) resp <- POST("https://chem.cogitopia.dev/check-definitions", body = toJSON(list(molecules = molecules), auto_unbox = TRUE), content_type_json()) data <- content(resp, "parsed") # Statistics for (def_id in names(data$statistics)) { s <- data$statistics[[def_id]] cat(sprintf("%-12s %d/%d matched ", def_id, s$matched, s$total)) } # Per-molecule match table rows <- do.call(rbind, lapply(data$results, function(r) { row <- data.frame(smiles = r$smiles, stringsAsFactors = FALSE) for (d in names(r$definitions)) row[[d]] <- r$definitions[[d]]$matched row })) print(rows)
library(httr); library(jsonlite) molecules <- c( "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)S(=O)(=O)O", # PFOS "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(=O)O", # PFOA "CCO" # ethanol ) # score = a*sum(|CF|) + b*percentile(|CF|, p) — same formula as the web interface # defaults: a=1.0, b=5.0, p=90, group=51 resp <- POST("https://chem.cogitopia.dev/prioritize", body = toJSON(list(molecules = molecules), auto_unbox = TRUE), content_type_json()) ranked <- content(resp, "parsed")$ranked df <- data.frame( rank = sapply(ranked, `[[`, "rank"), score = sapply(ranked, `[[`, "score"), max_size = sapply(ranked, `[[`, "max_size"), smiles = sapply(ranked, `[[`, "smiles"), stringsAsFactors = FALSE) print(df) # Custom: emphasise chain length (b=10, p=100 = max component only) resp2 <- POST("https://chem.cogitopia.dev/prioritize", body = toJSON(list(molecules = molecules, b = 10, p = 100), auto_unbox = TRUE), content_type_json()) cat(" Custom (b=10, p=100): ") for (r in content(resp2, "parsed")$ranked) cat(r$rank, r$score, r$smiles, " ")
curl -s -X POST https://chem.cogitopia.dev/analyze \ -H "Content-Type: application/json" \ -d '{ "input": "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)S(=O)(=O)O", "inputType": "smiles" }' | jq '.matches[] | " - " + .name + " (" + (.components | map(.SMARTS + " size: " + (.size|tostring)) | join(",")) + ")"'
curl -s -X POST https://chem.cogitopia.dev/analyze-batch \ -H "Content-Type: application/json" \ -d '{"molecules":[{"smiles":"FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)S(=O)(=O)O"},{"smiles":"FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(=O)O"}]}' \ | jq -r ' ["mol","SMILES","Group","type","#c","maxSz","sizes","totC"], (.results | to_entries[] | .key as $i | (if $i == 0 then "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)S(=O)(=O)O" else "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(=O)O" end) as $smi | .value.matches[] | (.components | map(.size)) as $sz | ((.name + " " + (.components | map(.SMARTS) | join(" ")) | ascii_downcase) | if test("perfluoro") then "perfluoro" elif test("polyfluoro") then "polyfluoro" else "-" end) as $typ | [ ($i+1|tostring), $smi, .name, $typ, (.components|length|tostring), ($sz|max|tostring), ($sz|tostring), ($sz|add|tostring) ] ) | @csv' > batch_results.csv && echo "Saved batch_results.csv"
# Get all groups curl https://chem.cogitopia.dev/groups # Get specific group by ID curl https://chem.cogitopia.dev/groups/1 # Save to file curl https://chem.cogitopia.dev/groups -o pfas_groups.json # Pretty print with jq curl -s https://chem.cogitopia.dev/groups | jq '.[0]'
curl -s -X POST https://chem.cogitopia.dev/check-definitions \ -H "Content-Type: application/json" \ -d '{"molecules":["FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)S(=O)(=O)O","FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(=O)O","CCO"]}' \ | jq '.results[] | {smiles, matched: [.definitions|to_entries[]|select(.value.matched).key]}'
# score = a*sum(|CF|) + b*percentile(|CF|, p) — same formula as the web interface # defaults: a=1.0, b=5.0, p=90, group=51 curl -s -X POST https://chem.cogitopia.dev/prioritize \ -H "Content-Type: application/json" \ -d '{"molecules":["FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)S(=O)(=O)O","FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(=O)O","CCO"]}' \ | jq -r '.ranked[] | "(.rank) score:(.score) max:(.max_size) (.smiles)"' # Custom: emphasise chain length (b=10, p=100) curl -s -X POST https://chem.cogitopia.dev/prioritize \ -H "Content-Type: application/json" \ -d '{"molecules":["FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)S(=O)(=O)O","FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(=O)O","CCO"],"b":10,"p":100}' \ | jq -r '.ranked[] | "(.rank) score:(.score) (.smiles)"'
# Analyze a single PFAS molecule $url = "https://chem.cogitopia.dev/analyze" $body = @{ input = "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)S(=O)(=O)O" inputType = "smiles" } | ConvertTo-Json $response = Invoke-RestMethod -Uri $url ` -Method Post ` -Body $body ` -ContentType "application/json" Write-Host "Matches found: $($response.matches.Count)" $response.matches | ForEach-Object { $compStr = ($_.components | ForEach-Object { "$($_.SMARTS) size: $($_.size)" }) -join "," Write-Host " - $($_.name) ($compStr)" }
# Single flat table — one row per group match per molecule $smiles = @( "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)S(=O)(=O)O", # PFOS "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(=O)O" # PFOA ) $body = @{ molecules = $smiles | ForEach-Object { @{ smiles = $_ } } } | ConvertTo-Json -Depth 5 $response = Invoke-RestMethod ` -Uri "https://chem.cogitopia.dev/analyze-batch" ` -Method Post -Body $body -ContentType "application/json" $rows = @() $i = 1 foreach ($smi in $smiles) { foreach ($m in $response.results[$i-1].matches) { $search = ($m.name + " " + (($m.components | ForEach-Object { $_.SMARTS }) -join " ")).ToLower() $typ = if ($search -match "perfluoro") { "perfluoro" } ` elseif ($search -match "polyfluoro") { "polyfluoro" } else { "-" } $sizes = $m.components | ForEach-Object { $_.size } $totC = ($sizes | Measure-Object -Sum).Sum $maxSz = ($sizes | Measure-Object -Maximum).Maximum $szStr = "[" + ($sizes -join ",") + "]" $rows += [PSCustomObject]@{ mol = $i; SMILES = $smi; Group = $m.name; type = $typ nc = $m.components.Count; maxSz = $maxSz; sizes = $szStr; totC = $totC } } $i++ } $smiW = ($smiles | ForEach-Object { $_.Length } | Measure-Object -Maximum).Maximum $fmt = "{0,3} {1,-$smiW} {2,-35} {3,-11} {4,2} {5,5} {6,-14} {7,4}" Write-Host ($fmt -f "mol","SMILES","Group","type","#c","maxSz","sizes","totC") Write-Host ("-" * 100) foreach ($r in $rows) { Write-Host ($fmt -f $r.mol, $r.SMILES, $r.Group, $r.type, ` $r.nc, $r.maxSz, $r.sizes, $r.totC) } $rows | Export-Csv "batch_results.csv" -NoTypeInformation Write-Host "Saved batch_results.csv"
# Check molecules against all 5 PFAS regulatory definitions $body = @{ molecules = @( "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)S(=O)(=O)O", # PFOS "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(=O)O", # PFOA "CCO" # ethanol ) } | ConvertTo-Json -Depth 3 $data = Invoke-RestMethod ` -Uri "https://chem.cogitopia.dev/check-definitions" ` -Method Post -Body $body -ContentType "application/json" # Statistics $data.statistics.PSObject.Properties | ForEach-Object { Write-Host "$($_.Name): $($_.Value.matched)/$($_.Value.total) matched" } # Per-molecule matched definitions $data.results | ForEach-Object { $r = $_ $matched = $r.definitions.PSObject.Properties | Where-Object { $_.Value.matched } | Select-Object -ExpandProperty Name Write-Host "$($r.smiles): [$($matched -join ', ')]" }
$smiles = @( "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)S(=O)(=O)O", # PFOS "FC(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(F)(F)C(=O)O", # PFOA "CCO" # ethanol ) # score = a*sum(|CF|) + b*percentile(|CF|, p) — same formula as the web interface # defaults: a=1.0, b=5.0, p=90, group=51 $body = @{ molecules = $smiles } | ConvertTo-Json $response = Invoke-RestMethod ` -Uri "https://chem.cogitopia.dev/prioritize" ` -Method Post -Body $body -ContentType "application/json" $response.ranked | Format-Table rank, score, max_size, smiles -AutoSize # Custom: emphasise chain length (b=10, p=100) $body2 = @{ molecules = $smiles; b = 10; p = 100 } | ConvertTo-Json $response2 = Invoke-RestMethod ` -Uri "https://chem.cogitopia.dev/prioritize" ` -Method Post -Body $body2 -ContentType "application/json" $response2.ranked | Format-Table rank, score, smiles -AutoSize