Classify any chemical structure into PFAS structural groups
Paste or upload SMILES structures to assess the presence of 73 PFAS structural groups. Results include fluorinated chain length.
Open AnalyzerEvaluate chemicals against 5 PFAS definitions
Test a list of molecules against five major regulatory definitions — OECD (2021), EU Restriction, US EPA OPPT (2023), UK, and PFASSTRUCTv5.
Open CheckerMatch mass-spectrometry peaks to known PFAS compounds
Upload a list of measured masses from an LC-MS or GC-MS experiment and search the FluoroDB database for matching PFAS compounds within a user-defined mass tolerance (ppm or Da)S.
Open Screening ToolExplore a curated database of fluorinated compounds
Search, filter, and export records from the FluoroDB fluorinated compound database. Filter by regulatory list membership, PFAS structural group, fluorinated chain length, molecular formula, and more. Each record links to its PFAS group classification and external identifiers (CAS, PubChem CID, DTXSID).
Open FluoroDBSpot patterns in families of fluorinated compounds
Explore chains of structurally related compounds that grow or shrink by one −CF₂− or −CFH− unit. Useful for identifying novel series members, understanding the chain-length distribution of fluorinated compounds, or tracking how regulatory list coverage changes within a homologous family.
Open ViewerFind non-PFAS alternatives for industrial applications
Browse the ZeroPM Alternatives Assessment Database: over 6,500 documented PFAS uses, the technical functions they serve, and ~3,400 assessed non-fluorinated replacements drawn from scientific and industry literature. Filter by use sector, function, or alternative type to find substitution options relevant to your application.
Open ExplorerDraw and validate chemical structures from SMILES
Render any SMILES or SMARTS string as a 2D structure diagram. Optionally run a SMARTS substructure search on a SMILES with highlighted matching atoms.
Open ViewerAccess the submission form
Contribute a newly identified PFAS use, technical function, or non-fluorinated substitute. Submissions are held for review by a project manager before being added to the public database, ensuring data quality while keeping the resource up to date.
Submit DataVerify PFAS group definitions with built-in test molecules
Run all 73 PFAS group definitions through their built-in example and counter-example molecules and inspect detailed pass / fail diagnostics. Designed for researchers and developers who are extending or auditing the group definitions, or checking that a custom SMARTS pattern behaves as expected.
Open ValidatorGET /health
POST /analyze
POST /analyze-batch
POST /check-definitions
GET /groups
GET /groups/:id
GET /definitions
POST /ms-candidates - Screen uploaded masses/files
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