PFAS Analysis Tools

PFAS Groups Analyzer

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 Analyzer

PFAS Definition Checker

Evaluate 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 Checker

MS Non-target Screening

Match mass-spectrometry peaks to known PFAS compounds

Open Screening Tool

FluoroDB Browser

Explore a curated database of fluorinated compounds

Open FluoroDB

Homologue Series Viewer

Open Viewer

PFAS Alternatives Database

Find 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 Explorer

SMILES Viewer

Draw 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 Viewer

Submit a New Alternative

Access 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 Data

PFAS Group Validator

Verify 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 Validator

API Endpoints

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

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