Singularis turns scientific PDFs into graphs of atomic research elements—Input Fact, Hypothesis, Experiment, Technique, Result, Dataset, Analysis, Conclusion—linked with typed edges. The goal is to reach 80–90% of an LLM read-out’s quality at 10–100× lower cost and latency; LLMs are used sparingly (refine/QA/auto-rule suggestion), while the core extraction is deterministic and cheap. Presentation
- grobid — PDF→TEI for S0
- api — FastAPI gateway (queues, preview/graph endpoints)
- worker — executes the S0→S1→S2 pipeline and writes artifacts
- web — React + Cytoscape viewer
- redis — RQ queue + job status store.
-
pipeline/pipeline/{s0_grobid.py, s1.py, s2.py, worker.py,…}— stages & orchestration -
ui/— web client -
rules/— data-driven themes (spaCy matcher/dep-matcher + lexicon) -
legacy_rules/— historical YAML rule set (kept for reference).
Spin up the full stack with Docker, run the pipeline on a PDF, and fetch the graph.
- Docker + Docker Compose
- A PDF to test with
-
Create a file
.envbased on the.env.examplefile (requires no changes) -
Launch the stack:
`docker compose up -d` # Services: # - GROBID on 8070/8071 # - API on 8000 # - Web UI on 3000 # - Redis on 6379 -
Open a web page: http://localhost:3000/
- Python 3.11, custom S1→S2 pipeline.
- spaCy: tokenization/lemmatization +
MatcherandDependencyMatcherfor rule-based patterns. - Custom JSON pattern sets:
matcher.json/depmatcher.json(common/… themes). - Lightweight heuristics for IMRAD, polarity, and numeric cues (no embeddings).
- S2 is plain Python: normalization/dedup, edge type validation & “retyping,” fallback connectivity, and layout for the frontend.
-
Input: flattened sentences from S0 with metadata (page, bbox, section/IMRAD).
-
Node type scoring:
- Run token/dep patterns; count hits for the 8 canonical types.
- Multipliers: IMRAD prior, base type weights, thematic boosts, hedge penalty; numeric-feature bonus.
- Tie-breaks and special nudges (e.g., in RESULTS/DISCUSSION,
Input Fact → Resultwhen dep-hits fire).
-
Polarity: light heuristic over lemmas/negation scope and stable phrases (e.g., “not significant”).
-
Node formation: assign
id,type,text,conf,polarity,prov(section/imrad/sent_idx/page/bbox). -
Initial edges (structural, no text-to-text semantic matching):
- Sliding window forward in text + thresholds on sentence distance and page distance; IMRAD direction checks.
- Edge types:
Technique→Experiment/Result: uses,Experiment→Result: produces,Result→Hypothesis: supports/refutes (from Result polarity),Dataset→Experiment/Analysis: feeds,Analysis→Result: informs, plus sanctionedResult→Result: followsandDataset→Result: summarizes. - Edge confidence: average of the two node confidences + “hysteresis” threshold vs. distance; tiny bonuses for “same section / neighboring IMRAD”.
- Plus linking via matched Fig./Table references (caption → “body”). (as summarized in your current setup)
-
Load & prep: take
s1_graph.json(and optionally S0 context for LLM payload). Eight canonical node types with fixed column order. -
Node normalization/dedup:
- Map synonyms to canonical types.
- Deduplicate by
(type, normalized_text, special context for Hypothesis), merging provenance and reconciling polarity.
-
Edge relinking/validation:
- Check each edge’s type-pair admissibility; if the relation is non-canonical but the pair is allowed, S2 retypes it to the canonical one (marking
retyped); if the pair is disallowed — drop it. - For
Result→Hypothesis, relation is chosen from polarity (supports/refutes). - Dedup edges
(from,to,type)by maxconf; if none remain — add a fallback backbone (minimal connectivity: Result→Hypothesis, Technique/Dataset→Experiment, Analysis→Conclusion, …).
- Check each edge’s type-pair admissibility; if the relation is non-canonical but the pair is allowed, S2 retypes it to the canonical one (marking
-
Optional LLM refinement: if connectivity is weak, assemble a compact payload (top nodes with sectional weights), call LLM to refine, then pass results through the same validations.
-
Frontend layout: assign columns/rows and textual ordering within columns:
(page, sent_idx, y, id)so the order matches S1. The result is written tograph.json.

