PFAS Analysis Tools

PFAS Groups Analyzer

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.

Open

PFAS Definition Checker

Check 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.

Open

PFAS Alternatives Database

Find non-PFAS alternatives for industrial applications

Browse the ZeroPM Alternatives Assessment Database by application, function, or hazard profile.

Open

SMILES Viewer

Draw and validate chemical structures from SMILES

Instantly render any SMILES or SMARTS string as a 2D structure diagram using RDKit-WASM.

Open

Submit a New Alternative

Contribute a PFAS use case or alternative

Fill in a structured form to submit a new PFAS application, function, or substitute compound for review.

Open

CAS to Structure

Resolve CAS numbers and names to chemical structures

Look up SMILES, InChI, formula, and synonyms for any compound via the PubChem PUG REST API.

Open

API Endpoints

GET /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

API Rate Limits

To ensure fair usage and server stability, the following rate limits apply per IP address:

  • General API endpoints 100/15min
    Applies to: GET /groups, /definitions, /alternatives-db/*
  • Analysis endpoints 20/15min
    Applies to: POST /analyze, /check-definitions, /ms-candidates
  • Batch processing 5/15min
    Applies to: POST /analyze-batch

Rate limit headers (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset) are included in all API responses.

API Usage Examples

Analyze a Molecule (Python)

Python
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']])})")

Batch Analysis & Component Table (Python)

Python
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}")

Get PFAS Groups (Python)

Python
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'])}")

Check Definitions (Python)

Python
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")

Prioritise by PFAS Group Count (Python)

Python
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']}")

Analyze a Molecule (R)

R
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")
}

Batch Analysis & Component Table (R)

R
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
")

Check Definitions (R)

R
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)

Prioritise by PFAS Group Count (R)

R
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, "
")

Analyze a Molecule (cURL - Linux/Mac)

Bash
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(",")) + ")"'

Batch Analysis & Component Table (cURL)

Bash
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 PFAS Groups (cURL)

Bash
# 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]'

Check Definitions (cURL)

Bash
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]}'

Prioritise by PFAS Group Count (cURL)

Bash
# 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 Molecule (PowerShell)

PowerShell
# 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)"
}

Batch Analysis & Component Table (PowerShell)

PowerShell
# 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 Definitions (PowerShell)

PowerShell
# 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 ', ')]"
}

Prioritise by PFAS Group Count (PowerShell)

PowerShell
$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
Climate Responsible Hosting: This web application is hosted on Infomaniak servers powered by 100% local renewable energy (60% hydrolic power and 40% solar and other green power). The data center recovers and redistributs its waste heat to neighboring households.
🔐 Manager Login