unicode-logic-kit 0.31.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- unicode_logic_kit/__init__.py +385 -0
- unicode_logic_kit/__main__.py +520 -0
- unicode_logic_kit/_deadline.py +219 -0
- unicode_logic_kit/ace/__init__.py +126 -0
- unicode_logic_kit/ace/_align.py +135 -0
- unicode_logic_kit/ace/chem_lexicon.py +128 -0
- unicode_logic_kit/ace/drs_reader.py +570 -0
- unicode_logic_kit/ace/mapping.py +666 -0
- unicode_logic_kit/ace/reverse_modal.py +138 -0
- unicode_logic_kit/ace/runner.py +551 -0
- unicode_logic_kit/ace/translate.py +452 -0
- unicode_logic_kit/ace/verbalize.py +1070 -0
- unicode_logic_kit/api.py +1284 -0
- unicode_logic_kit/atp/__init__.py +177 -0
- unicode_logic_kit/atp/_ascii_names.py +113 -0
- unicode_logic_kit/atp/_html.py +72 -0
- unicode_logic_kit/atp/_substructural_input.py +228 -0
- unicode_logic_kit/atp/_tff_problem.py +715 -0
- unicode_logic_kit/atp/_tptp_problem.py +1111 -0
- unicode_logic_kit/atp/_writer_support.py +289 -0
- unicode_logic_kit/atp/clingo_backend.py +1180 -0
- unicode_logic_kit/atp/cvc5_backend.py +1385 -0
- unicode_logic_kit/atp/eprover_backend.py +732 -0
- unicode_logic_kit/atp/finite_domain.py +1055 -0
- unicode_logic_kit/atp/fitch.py +1547 -0
- unicode_logic_kit/atp/fitch_search.py +551 -0
- unicode_logic_kit/atp/hets_backend.py +339 -0
- unicode_logic_kit/atp/hybrid_down.py +120 -0
- unicode_logic_kit/atp/incremental.py +250 -0
- unicode_logic_kit/atp/kripke_enum.py +741 -0
- unicode_logic_kit/atp/lambek.py +436 -0
- unicode_logic_kit/atp/leo3_backend.py +332 -0
- unicode_logic_kit/atp/linear.py +738 -0
- unicode_logic_kit/atp/lj.py +705 -0
- unicode_logic_kit/atp/logic_backends.py +566 -0
- unicode_logic_kit/atp/ltl_tableau.py +1084 -0
- unicode_logic_kit/atp/minizinc_backend.py +1402 -0
- unicode_logic_kit/atp/modal_tableau.py +1382 -0
- unicode_logic_kit/atp/nanocop_backend.py +410 -0
- unicode_logic_kit/atp/portfolio.py +489 -0
- unicode_logic_kit/atp/protocol.py +1803 -0
- unicode_logic_kit/atp/prover9_entailment.py +1153 -0
- unicode_logic_kit/atp/resolution.py +1376 -0
- unicode_logic_kit/atp/resolution_check.py +1114 -0
- unicode_logic_kit/atp/sequent.py +1050 -0
- unicode_logic_kit/atp/tableau.py +921 -0
- unicode_logic_kit/atp/tableau_check.py +543 -0
- unicode_logic_kit/atp/tptp_ncl.py +811 -0
- unicode_logic_kit/atp/tptp_tff.py +1546 -0
- unicode_logic_kit/atp/tstp.py +1333 -0
- unicode_logic_kit/atp/tstp_check.py +1096 -0
- unicode_logic_kit/atp/twee_backend.py +236 -0
- unicode_logic_kit/atp/twee_check.py +711 -0
- unicode_logic_kit/atp/twee_entailment.py +953 -0
- unicode_logic_kit/atp/vampire_entailment.py +540 -0
- unicode_logic_kit/atp/z3_arith.py +470 -0
- unicode_logic_kit/atp/z3_equivalence.py +36 -0
- unicode_logic_kit/atp/z3_fuzzy.py +362 -0
- unicode_logic_kit/atp/z3_input.py +500 -0
- unicode_logic_kit/atp/z3_models.py +208 -0
- unicode_logic_kit/chem/__init__.py +88 -0
- unicode_logic_kit/chem/_naming.py +284 -0
- unicode_logic_kit/chem/cache.py +185 -0
- unicode_logic_kit/chem/interop.py +244 -0
- unicode_logic_kit/chem/mol.py +525 -0
- unicode_logic_kit/chem/signature.py +112 -0
- unicode_logic_kit/comorphism.py +497 -0
- unicode_logic_kit/dl/__init__.py +384 -0
- unicode_logic_kit/dl/classification.py +227 -0
- unicode_logic_kit/dl/concepts.py +632 -0
- unicode_logic_kit/dl/datatypes.py +818 -0
- unicode_logic_kit/dl/owl_functional.py +2433 -0
- unicode_logic_kit/dl/owl_manchester.py +1637 -0
- unicode_logic_kit/dl/owl_reasoner.py +790 -0
- unicode_logic_kit/dl/parser.py +391 -0
- unicode_logic_kit/dl/tableau.py +4048 -0
- unicode_logic_kit/dl/translate.py +2704 -0
- unicode_logic_kit/drt/__init__.py +94 -0
- unicode_logic_kit/drt/export.py +179 -0
- unicode_logic_kit/drt/nodes.py +506 -0
- unicode_logic_kit/drt/parser.py +965 -0
- unicode_logic_kit/drt/resolve.py +195 -0
- unicode_logic_kit/drt/reverse.py +175 -0
- unicode_logic_kit/eval/__init__.py +106 -0
- unicode_logic_kit/eval/batch.py +382 -0
- unicode_logic_kit/eval/canonical.py +663 -0
- unicode_logic_kit/eval/chem_batch.py +606 -0
- unicode_logic_kit/eval/converses.py +200 -0
- unicode_logic_kit/eval/datasets/__init__.py +136 -0
- unicode_logic_kit/eval/datasets/_base.py +263 -0
- unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
- unicode_logic_kit/eval/datasets/c3po.py +678 -0
- unicode_logic_kit/eval/datasets/folio.py +158 -0
- unicode_logic_kit/eval/datasets/fracas.py +418 -0
- unicode_logic_kit/eval/datasets/groves.py +191 -0
- unicode_logic_kit/eval/datasets/logicbench.py +467 -0
- unicode_logic_kit/eval/datasets/logicnli.py +303 -0
- unicode_logic_kit/eval/datasets/malls.py +133 -0
- unicode_logic_kit/eval/datasets/pfolio.py +594 -0
- unicode_logic_kit/eval/datasets/pmb.py +242 -0
- unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
- unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
- unicode_logic_kit/eval/datasets/proverqa.py +674 -0
- unicode_logic_kit/eval/datasets/willow.py +478 -0
- unicode_logic_kit/eval/equivalence.py +466 -0
- unicode_logic_kit/eval/exercise_gen.py +533 -0
- unicode_logic_kit/eval/explain.py +791 -0
- unicode_logic_kit/eval/generality.py +750 -0
- unicode_logic_kit/eval/metric_hf.py +458 -0
- unicode_logic_kit/eval/predicate_match.py +343 -0
- unicode_logic_kit/eval/theory_check.py +1170 -0
- unicode_logic_kit/eval/validate.py +306 -0
- unicode_logic_kit/fol/__init__.py +177 -0
- unicode_logic_kit/fol/_atom_keys.py +510 -0
- unicode_logic_kit/fol/_fol_nodes.py +3586 -0
- unicode_logic_kit/fol/_free_parameters.py +105 -0
- unicode_logic_kit/fol/_ho_nodes.py +448 -0
- unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
- unicode_logic_kit/fol/_identifiers.py +1091 -0
- unicode_logic_kit/fol/_lambek_nodes.py +112 -0
- unicode_logic_kit/fol/_linear_nodes.py +352 -0
- unicode_logic_kit/fol/_modal_nodes.py +1467 -0
- unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
- unicode_logic_kit/fol/_numeral_symbols.py +231 -0
- unicode_logic_kit/fol/_so_nodes.py +200 -0
- unicode_logic_kit/fol/_symbol_names.py +81 -0
- unicode_logic_kit/fol/_team_nodes.py +181 -0
- unicode_logic_kit/fol/_tptp_symbols.py +551 -0
- unicode_logic_kit/fol/_truth_constants.py +117 -0
- unicode_logic_kit/fol/casl_export.py +1135 -0
- unicode_logic_kit/fol/casl_import.py +929 -0
- unicode_logic_kit/fol/derivation.py +367 -0
- unicode_logic_kit/fol/dialect_detect.py +70 -0
- unicode_logic_kit/fol/dialect_repair.py +537 -0
- unicode_logic_kit/fol/frames.py +637 -0
- unicode_logic_kit/fol/grammars/terminals.lark +31 -0
- unicode_logic_kit/fol/lambda_tools.py +297 -0
- unicode_logic_kit/fol/latex_input.py +429 -0
- unicode_logic_kit/fol/modal_translation.py +944 -0
- unicode_logic_kit/fol/msflparser.py +1033 -0
- unicode_logic_kit/fol/naming.py +422 -0
- unicode_logic_kit/fol/nodes.py +241 -0
- unicode_logic_kit/fol/normalforms.py +492 -0
- unicode_logic_kit/fol/pal.py +287 -0
- unicode_logic_kit/fol/prolog_export.py +566 -0
- unicode_logic_kit/fol/prolog_input.py +505 -0
- unicode_logic_kit/fol/prover9_input.py +1325 -0
- unicode_logic_kit/fol/qml.py +1760 -0
- unicode_logic_kit/fol/qmltp_input.py +525 -0
- unicode_logic_kit/fol/sanitize.py +221 -0
- unicode_logic_kit/fol/serialize.py +79 -0
- unicode_logic_kit/fol/signature.py +1290 -0
- unicode_logic_kit/fol/simplify_check.py +544 -0
- unicode_logic_kit/fol/spans.py +594 -0
- unicode_logic_kit/fol/tptp_input.py +1503 -0
- unicode_logic_kit/fol/tptp_repair.py +941 -0
- unicode_logic_kit/fol/unification.py +157 -0
- unicode_logic_kit/fol/verbalize.py +263 -0
- unicode_logic_kit/hets/__init__.py +163 -0
- unicode_logic_kit/hets/bridge.py +142 -0
- unicode_logic_kit/hets/client.py +748 -0
- unicode_logic_kit/hets/docker.py +420 -0
- unicode_logic_kit/hets/dol.py +712 -0
- unicode_logic_kit/hets/haskell_json.py +355 -0
- unicode_logic_kit/hets/owl_backend.py +794 -0
- unicode_logic_kit/hets/owl_cli.py +598 -0
- unicode_logic_kit/hets/symbols.py +512 -0
- unicode_logic_kit/hol/__init__.py +140 -0
- unicode_logic_kit/hol/_ho_common.py +323 -0
- unicode_logic_kit/hol/_isabelle_binders.py +125 -0
- unicode_logic_kit/hol/classical.py +812 -0
- unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
- unicode_logic_kit/hol/deepshallow/_common.py +177 -0
- unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
- unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
- unicode_logic_kit/hol/deepshallow/modal.py +217 -0
- unicode_logic_kit/hol/deepshallow/qml.py +406 -0
- unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
- unicode_logic_kit/hol/free.py +753 -0
- unicode_logic_kit/hol/goedel.py +336 -0
- unicode_logic_kit/hol/ho_modal.py +1743 -0
- unicode_logic_kit/hol/intuitionistic.py +403 -0
- unicode_logic_kit/hol/isabelle_conditional.py +593 -0
- unicode_logic_kit/hol/isabelle_modal.py +1908 -0
- unicode_logic_kit/hol/isabelle_relevant.py +412 -0
- unicode_logic_kit/hol/isabelle_runner.py +1147 -0
- unicode_logic_kit/hol/isabelle_substructural.py +884 -0
- unicode_logic_kit/hol/lean.py +1018 -0
- unicode_logic_kit/hol/manyvalued.py +921 -0
- unicode_logic_kit/hol/secondorder.py +687 -0
- unicode_logic_kit/hol/thf_modal.py +941 -0
- unicode_logic_kit/hol/thirdorder.py +397 -0
- unicode_logic_kit/ilp/__init__.py +89 -0
- unicode_logic_kit/ilp/readback.py +389 -0
- unicode_logic_kit/ilp/separation.py +153 -0
- unicode_logic_kit/ilp/task.py +730 -0
- unicode_logic_kit/logic.py +163 -0
- unicode_logic_kit/mcp/__init__.py +28 -0
- unicode_logic_kit/mcp/__main__.py +5 -0
- unicode_logic_kit/mcp/chem_tools.py +1031 -0
- unicode_logic_kit/mcp/server.py +2453 -0
- unicode_logic_kit/mcp/syntax_spec.py +681 -0
- unicode_logic_kit/prob/__init__.py +53 -0
- unicode_logic_kit/prob/_bdd.py +225 -0
- unicode_logic_kit/prob/_column_gen.py +668 -0
- unicode_logic_kit/prob/distribution.py +686 -0
- unicode_logic_kit/prob/nilsson.py +470 -0
- unicode_logic_kit/py.typed +0 -0
- unicode_logic_kit/semantics/__init__.py +137 -0
- unicode_logic_kit/semantics/_modal_reject.py +156 -0
- unicode_logic_kit/semantics/action_models.py +466 -0
- unicode_logic_kit/semantics/asp_models.py +1200 -0
- unicode_logic_kit/semantics/conditional.py +580 -0
- unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
- unicode_logic_kit/semantics/free_logic.py +913 -0
- unicode_logic_kit/semantics/fuzzy.py +384 -0
- unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
- unicode_logic_kit/semantics/intuitionistic.py +581 -0
- unicode_logic_kit/semantics/kripke.py +1139 -0
- unicode_logic_kit/semantics/manyvalued.py +580 -0
- unicode_logic_kit/semantics/matrix.py +342 -0
- unicode_logic_kit/semantics/model_eval.py +1135 -0
- unicode_logic_kit/semantics/modelfinder.py +1036 -0
- unicode_logic_kit/semantics/nonmonotonic.py +372 -0
- unicode_logic_kit/semantics/relevant.py +331 -0
- unicode_logic_kit/semantics/secondorder.py +657 -0
- unicode_logic_kit/semantics/structures.py +352 -0
- unicode_logic_kit/semantics/tarski.py +975 -0
- unicode_logic_kit/semantics/team.py +315 -0
- unicode_logic_kit/semantics/team_translation.py +416 -0
- unicode_logic_kit/semantics/thirdorder.py +358 -0
- unicode_logic_kit/semantics/tnorm.py +85 -0
- unicode_logic_kit/semantics/truthtable.py +201 -0
- unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
- unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
- unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
- unicode_logic_kit-0.31.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
"""Adapter for C3PO (CHEBI Chemical Classification Program Ontology
|
|
2
|
+
Benchmark, Mungall, Malik, Korn, Reese, O'Boyle & Hastings, "Chemical
|
|
3
|
+
classification program synthesis using generative artificial intelligence",
|
|
4
|
+
Journal of Cheminformatics, 2025, DOI 10.1186/s13321-025-01092-3) — local
|
|
5
|
+
JSONL only, no network access.
|
|
6
|
+
|
|
7
|
+
C3PO pairs each CHEBI class with its natural-language definition plus the
|
|
8
|
+
SMILES of molecules known to be members -- the shape any FOL formalisation
|
|
9
|
+
of chemical classes has to be evaluated against. Unlike every other adapter
|
|
10
|
+
in this subpackage, C3PO's gold is NOT a reference FOL formula to compare a
|
|
11
|
+
prediction against by parsing/structural-equality (there is no gold FOL
|
|
12
|
+
here at all — see "Honesty" below); it is an EXECUTABLE membership decision:
|
|
13
|
+
a molecule either is or is not in the class, decidable by MODEL CHECKING a
|
|
14
|
+
candidate FOL definition against the molecule as a
|
|
15
|
+
:class:`~unicode_logic_kit.semantics.structures.FiniteStructure`
|
|
16
|
+
(:func:`unicode_logic_kit.chem.mol_to_structure`). This module accordingly
|
|
17
|
+
ships two things: :func:`load_c3po` (the usual streaming loader) and
|
|
18
|
+
:func:`score_definition`, which runs that model-checking evaluation over a
|
|
19
|
+
positive/negative SMILES split and reports precision/recall/F1 — WITHOUT
|
|
20
|
+
silently folding a failed evaluation into "negative" (see its own docstring).
|
|
21
|
+
|
|
22
|
+
Source and verified schema
|
|
23
|
+
---------------------------
|
|
24
|
+
Source: https://huggingface.co/datasets/MonarchInit/C3PO (unauthenticated,
|
|
25
|
+
CC0-1.0). Verified directly, 2026-08-13:
|
|
26
|
+
|
|
27
|
+
* The HF dataset-viewer's queryable ``"default"``/``"train"`` config (via the
|
|
28
|
+
``datasets-server`` ``/first-rows`` API) exposes exactly the CLASSES table,
|
|
29
|
+
9 flat fields, confirmed against real fetched rows (CHEBI:10036 "wax
|
|
30
|
+
ester", CHEBI:131565 "steroid aldehyde") AND cross-checked against the
|
|
31
|
+
``ChemicalClass`` Pydantic model in the benchmark's own generator source,
|
|
32
|
+
https://github.com/cmungall/c3p (``c3p/datamodel.py``, fetched directly),
|
|
33
|
+
which agrees field-for-field:
|
|
34
|
+
|
|
35
|
+
* ``"id"`` -- ``str``, a CHEBI curie (e.g.
|
|
36
|
+
``"CHEBI:10036"``) -- globally unique, unlike FOLIO's/ProofWriter's
|
|
37
|
+
grouping ids (see their modules' "Id resolution" notes) -- so this
|
|
38
|
+
loader uses it directly as :attr:`DatasetExample.id`, no positional
|
|
39
|
+
fallback scheme needed for the common case.
|
|
40
|
+
* ``"name"`` -- ``str``, the class's ``rdfs:label``.
|
|
41
|
+
* ``"definition"`` -- ``str``, the natural-language
|
|
42
|
+
definition an FOL-formalisation system (an LLM, or a human) is
|
|
43
|
+
asked to translate. THIS is what :func:`load_c3po` maps onto
|
|
44
|
+
``nl_conclusion`` -- see "Field mapping" below.
|
|
45
|
+
* ``"parents"`` -- list of parent-class CHEBI curies.
|
|
46
|
+
* ``"xrefs"`` -- list of cross-references to other
|
|
47
|
+
databases (KEGG, MetaCyc, ...); absent for most classes (confirmed via
|
|
48
|
+
the mirror's own column statistics: only 367 of 1364 classes carry any).
|
|
49
|
+
* ``"all_positive_examples"`` -- list of SMILES strings: molecules
|
|
50
|
+
CHEBI records as members of this class. THE gold used by
|
|
51
|
+
:func:`score_definition`'s ``positives=`` argument.
|
|
52
|
+
* ``"parents_count"`` / ``"xrefs_count"`` / ``"all_positive_examples_count"``
|
|
53
|
+
-- ``int``/``float``/``int`` -- redundant with the length of the
|
|
54
|
+
corresponding list field (kept verbatim, not recomputed, in case a real
|
|
55
|
+
export ever has them drift).
|
|
56
|
+
|
|
57
|
+
Note for anyone working from prose descriptions of this benchmark rather
|
|
58
|
+
than the verified schema: there is NO "number of transitive subclasses" field
|
|
59
|
+
anywhere in this table -- ``parents_count`` counts DIRECT PARENTS, the
|
|
60
|
+
opposite direction. Whatever prior description mentioned a transitive-
|
|
61
|
+
subclass count was not corroborated by the live schema and this loader
|
|
62
|
+
does not invent one.
|
|
63
|
+
|
|
64
|
+
* **``structures.csv``** (every CHEBI structure with its SMILES and its
|
|
65
|
+
class memberships, referenced by the README as containing "a flag ... [for
|
|
66
|
+
the] validation split") is a REAL sibling file in the same HF repo but
|
|
67
|
+
could NOT be verified at the row level: it is a 38.5 MB Git-LFS blob, over
|
|
68
|
+
this environment's fetch size limit, is not exposed through the
|
|
69
|
+
``datasets-server`` rows API (only the classes table is), and the repo has
|
|
70
|
+
no successful parquet auto-conversion to sample instead (checked: the
|
|
71
|
+
``/parquet`` endpoint reports a failed conversion). The upstream Pydantic
|
|
72
|
+
model (``c3p/datamodel.py``'s ``ChemicalStructure``) declares only
|
|
73
|
+
``name``/``smiles``; the validation-split flag the README describes is not
|
|
74
|
+
visible on that model at all, so its ACTUAL column name in the exported
|
|
75
|
+
CSV is unverified. Recorded honestly rather than glossed over: **this
|
|
76
|
+
adapter does not parse ``structures.csv``** rather than guess a column
|
|
77
|
+
name that could silently mislabel every molecule's split. This is not a
|
|
78
|
+
functional gap for the documented use: :func:`score_definition` takes its
|
|
79
|
+
``positives``/``negatives`` SMILES directly from the CALLER (typically
|
|
80
|
+
``example.meta["positive_smiles"]`` for positives, plus whatever negative
|
|
81
|
+
pool the caller assembles) rather than reading a structures table itself.
|
|
82
|
+
|
|
83
|
+
License: **CC0-1.0** (public-domain dedication), per the repo's own
|
|
84
|
+
``cardData``/tags -- verified directly, not inferred. Unlike FOLIO
|
|
85
|
+
(CC-BY-SA-4.0, share-alike) or MALLS (CC-BY-NC-4.0, non-commercial), C3PO
|
|
86
|
+
carries no redistribution restriction at all; this loader still never
|
|
87
|
+
downloads or embeds the real data (see below), simply because there is no
|
|
88
|
+
license reason it would need to.
|
|
89
|
+
|
|
90
|
+
Field mapping
|
|
91
|
+
--------------
|
|
92
|
+
C3PO has no premise/conclusion entailment structure (like MALLS, not like
|
|
93
|
+
FOLIO) AND no gold FOL of any kind (like ProofWriter's flat NL split, not
|
|
94
|
+
like FOLIO/MALLS) -- see "Honesty" above. Mapped onto
|
|
95
|
+
:class:`~unicode_logic_kit.eval.datasets.DatasetExample`:
|
|
96
|
+
|
|
97
|
+
* ``id`` -- the record's own ``"id"`` (a CHEBI curie) verbatim, or the
|
|
98
|
+
positional fallback ``f"c3po:{line_no}"`` for a record with none.
|
|
99
|
+
* ``nl_premises`` / ``fol_premises`` -- always ``()`` (no premise structure).
|
|
100
|
+
* ``nl_conclusion`` -- ``"definition"`` verbatim (the NL text a
|
|
101
|
+
formalisation system is asked to translate).
|
|
102
|
+
* ``fol_conclusion`` -- always ``None`` (C3PO ships no FOL gold at all;
|
|
103
|
+
consequently :func:`~unicode_logic_kit.eval.datasets.audit_examples` run over
|
|
104
|
+
C3PO examples is VACUOUSLY ``ok=True`` for every one of them -- exactly the
|
|
105
|
+
same caveat as :mod:`~unicode_logic_kit.eval.datasets.proofwriter`'s
|
|
106
|
+
"Honesty" section, for the identical reason: nothing to parse means
|
|
107
|
+
nothing can fail to parse).
|
|
108
|
+
* ``label`` -- always ``None``. C3PO's "gold" is not a single categorical
|
|
109
|
+
label per example (unlike FOLIO's True/False/Uncertain) -- it is the
|
|
110
|
+
per-MOLECULE membership decision :func:`score_definition` computes, which
|
|
111
|
+
does not fit this dataclass's one-label-per-example slot.
|
|
112
|
+
* ``meta`` -- ``"chebi_id"`` (same value as ``id``, kept explicit alongside
|
|
113
|
+
it), ``"name"``, ``"positive_smiles"`` (tuple, from
|
|
114
|
+
``"all_positive_examples"`` -- the field :func:`score_definition`'s
|
|
115
|
+
``positives=`` argument is meant to be filled from), ``"parents"``,
|
|
116
|
+
``"parents_count"``, ``"xrefs"``, ``"xrefs_count"``,
|
|
117
|
+
``"all_positive_examples_count"``, ``"line_no"``, plus any other key the
|
|
118
|
+
record happens to carry (forward-compatibility, mirroring every other
|
|
119
|
+
adapter in this subpackage).
|
|
120
|
+
|
|
121
|
+
This module never downloads anything. The real C3PO ``classes.csv`` is a
|
|
122
|
+
CSV with (per the verified schema above) LIST-valued cells for
|
|
123
|
+
``parents``/``xrefs``/``all_positive_examples``, whose exact upstream
|
|
124
|
+
serialisation delimiter could not itself be verified (the raw CSV bytes were
|
|
125
|
+
unreachable -- see "structures.csv" above for why); rather than guess a
|
|
126
|
+
delimiter that might silently mis-split a SMILES string containing the wrong
|
|
127
|
+
character, :func:`load_c3po` reads local JSONL with the SAME COLUMN NAMES
|
|
128
|
+
and plain JSON list values for those three fields -- trivially produced from
|
|
129
|
+
a real download with, e.g.::
|
|
130
|
+
|
|
131
|
+
import ast, json, pandas as pd
|
|
132
|
+
df = pd.read_csv("classes.csv")
|
|
133
|
+
for col in ("parents", "xrefs", "all_positive_examples"):
|
|
134
|
+
df[col] = df[col].apply(lambda v: ast.literal_eval(v) if isinstance(v, str) else v)
|
|
135
|
+
df.to_json("classes.jsonl", orient="records", lines=True)
|
|
136
|
+
|
|
137
|
+
(swap ``ast.literal_eval`` for ``json.loads`` if a real download turns out to
|
|
138
|
+
already use JSON-list syntax in its cells -- unverified either way here).
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
import json
|
|
142
|
+
from pathlib import Path
|
|
143
|
+
from typing import (
|
|
144
|
+
FrozenSet, Iterable, Iterator, List, MutableMapping, Optional,
|
|
145
|
+
Tuple, Union,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
from dataclasses import dataclass
|
|
149
|
+
|
|
150
|
+
from ...fol.nodes import Node
|
|
151
|
+
from ...fol.tptp_input import TptpParsingError
|
|
152
|
+
from ...chem import mol_to_structure, parse_chemlog_tptp, to_chemlog_names
|
|
153
|
+
from ...chem.cache import StructureBuildError
|
|
154
|
+
from ...semantics.model_eval import (
|
|
155
|
+
evaluate_detailed, UninterpretedSymbol, UnsupportedNode,
|
|
156
|
+
)
|
|
157
|
+
from ...semantics.structures import FiniteStructure
|
|
158
|
+
from ._base import DatasetExample, _register_dataset_info
|
|
159
|
+
|
|
160
|
+
__all__ = ["load_c3po", "DefinitionScore", "score_definition"]
|
|
161
|
+
|
|
162
|
+
_register_dataset_info(
|
|
163
|
+
"c3po",
|
|
164
|
+
license="CC0-1.0",
|
|
165
|
+
source_url="https://huggingface.co/datasets/MonarchInit/C3PO",
|
|
166
|
+
citation_hint=(
|
|
167
|
+
"Mungall, Christopher J., Adnan Malik, Daniel R. Korn, Justin T. "
|
|
168
|
+
"Reese, Noel M. O'Boyle, and Janna Hastings. \"Chemical "
|
|
169
|
+
"classification program synthesis using generative artificial "
|
|
170
|
+
"intelligence.\" Journal of Cheminformatics (2025). "
|
|
171
|
+
"DOI:10.1186/s13321-025-01092-3."
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# The verified classes.csv/first-rows column set (see module docstring).
|
|
176
|
+
_KNOWN_KEYS = frozenset({
|
|
177
|
+
"id", "name", "definition", "parents", "xrefs", "all_positive_examples",
|
|
178
|
+
"parents_count", "xrefs_count", "all_positive_examples_count",
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
# load_c3po
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
def _example_from_record(record: dict, line_no: int,
|
|
187
|
+
known_bad_ids: FrozenSet[str]) -> DatasetExample:
|
|
188
|
+
chebi_id = record.get("id")
|
|
189
|
+
example_id = str(chebi_id) if chebi_id is not None else f"c3po:{line_no}"
|
|
190
|
+
definition = record.get("definition")
|
|
191
|
+
positive_smiles = tuple(record.get("all_positive_examples") or ())
|
|
192
|
+
|
|
193
|
+
meta = {
|
|
194
|
+
"chebi_id": chebi_id,
|
|
195
|
+
"name": record.get("name"),
|
|
196
|
+
"positive_smiles": positive_smiles,
|
|
197
|
+
"parents": tuple(record.get("parents") or ()),
|
|
198
|
+
"parents_count": record.get("parents_count"),
|
|
199
|
+
"xrefs": tuple(record.get("xrefs") or ()),
|
|
200
|
+
"xrefs_count": record.get("xrefs_count"),
|
|
201
|
+
"all_positive_examples_count": record.get("all_positive_examples_count"),
|
|
202
|
+
}
|
|
203
|
+
# Forward-compatibility: preserve any key this record carries beyond the
|
|
204
|
+
# verified schema, same convention as every other adapter here.
|
|
205
|
+
for key, value in record.items():
|
|
206
|
+
if key not in _KNOWN_KEYS:
|
|
207
|
+
meta.setdefault(key, value)
|
|
208
|
+
meta["line_no"] = line_no
|
|
209
|
+
|
|
210
|
+
return DatasetExample(
|
|
211
|
+
id=example_id,
|
|
212
|
+
nl_premises=(),
|
|
213
|
+
fol_premises=(),
|
|
214
|
+
nl_conclusion=definition,
|
|
215
|
+
fol_conclusion=None,
|
|
216
|
+
label=None,
|
|
217
|
+
known_bad=example_id in known_bad_ids,
|
|
218
|
+
meta=meta,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def load_c3po(path: Union[str, Path], *,
|
|
223
|
+
known_bad_ids: FrozenSet[str] = frozenset()) -> Iterator[DatasetExample]:
|
|
224
|
+
"""Stream :class:`~unicode_logic_kit.eval.datasets.DatasetExample` from a
|
|
225
|
+
local C3PO classes JSONL file (verified schema -- see module docstring).
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
path: path to a local ``.jsonl`` file -- one ``{"id", "name",
|
|
229
|
+
"definition", "parents", "xrefs", "all_positive_examples",
|
|
230
|
+
"parents_count", "xrefs_count", "all_positive_examples_count"}``
|
|
231
|
+
object per non-blank line (see module docstring for producing
|
|
232
|
+
this from a real ``classes.csv`` download). NEVER downloaded by
|
|
233
|
+
this function.
|
|
234
|
+
known_bad_ids: ids (the record's own CHEBI curie, or the positional
|
|
235
|
+
fallback ``f"c3po:{line_no}"`` for a record with none) whose
|
|
236
|
+
``all_positive_examples`` is known to be broken (e.g. a SMILES
|
|
237
|
+
that fails to parse, found by a prior audit). Every yielded
|
|
238
|
+
example with a matching id gets ``known_bad=True``.
|
|
239
|
+
|
|
240
|
+
Yields:
|
|
241
|
+
One :class:`~unicode_logic_kit.eval.datasets.DatasetExample` per
|
|
242
|
+
non-blank JSONL line, in file order -- see module docstring's "Field
|
|
243
|
+
mapping" for exactly what goes where. ``fol_premises`` is always
|
|
244
|
+
``()`` and ``fol_conclusion`` is always ``None`` (C3PO has no FOL
|
|
245
|
+
gold of any kind).
|
|
246
|
+
|
|
247
|
+
Raises:
|
|
248
|
+
FileNotFoundError: ``path`` does not exist.
|
|
249
|
+
json.JSONDecodeError: a non-blank line is not valid JSON -- raised,
|
|
250
|
+
not swallowed (a malformed dataset file must fail loudly).
|
|
251
|
+
"""
|
|
252
|
+
path = Path(path)
|
|
253
|
+
with path.open("r", encoding="utf-8") as fh:
|
|
254
|
+
for line_no, raw_line in enumerate(fh):
|
|
255
|
+
line = raw_line.strip()
|
|
256
|
+
if not line:
|
|
257
|
+
continue
|
|
258
|
+
record = json.loads(line)
|
|
259
|
+
yield _example_from_record(record, line_no, known_bad_ids)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
# score_definition -- model-checking a candidate FOL definition
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
@dataclass(frozen=True)
|
|
267
|
+
class DefinitionScore:
|
|
268
|
+
"""Outcome of :func:`score_definition`: a confusion matrix PLUS two
|
|
269
|
+
honest failure categories that are never folded into it.
|
|
270
|
+
|
|
271
|
+
``tp``/``fp``/``fn``/``tn`` count only molecules the evaluator reached a
|
|
272
|
+
DEFINITE two-valued verdict on. ``n_errors`` (with per-item detail in
|
|
273
|
+
``errors``) counts molecules where building the structure or evaluating
|
|
274
|
+
the formula raised one of the documented, expected exceptions (an
|
|
275
|
+
unparseable/invalid SMILES; a formula predicate/constant the structure
|
|
276
|
+
does not interpret; a formula outside the evaluator's supported
|
|
277
|
+
fragment; a free variable) -- these are NEITHER a positive NOR a
|
|
278
|
+
negative prediction, so they must never silently become one. Likewise
|
|
279
|
+
``n_exhausted`` (``exhausted``) counts molecules where the evaluator's
|
|
280
|
+
step ``budget`` ran out before a definite answer -- an honest UNKNOWN,
|
|
281
|
+
not a guessed ``False``. ``n_positives``/``n_negatives`` are the raw
|
|
282
|
+
input counts, so ``tp + fn + (errors/exhausted tagged "positive") ==
|
|
283
|
+
n_positives`` always holds (and symmetrically for negatives) -- every
|
|
284
|
+
input molecule is accounted for exactly once, in exactly one bucket.
|
|
285
|
+
|
|
286
|
+
``precision``/``recall``/``f1`` are ``None`` when their denominator would
|
|
287
|
+
be zero (e.g. ``precision`` needs at least one of ``tp``/``fp``) --
|
|
288
|
+
never silently reported as ``0.0``, which would misrepresent "no data"
|
|
289
|
+
as "definitely wrong". When both ``precision`` and ``recall`` ARE
|
|
290
|
+
defined but sum to zero (both exactly 0.0), ``f1`` is reported as
|
|
291
|
+
``0.0`` by the standard convention (the ``2pr/(p+r)`` formula's own
|
|
292
|
+
removable singularity at the origin), not ``None``.
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
tp: int
|
|
296
|
+
fp: int
|
|
297
|
+
fn: int
|
|
298
|
+
tn: int
|
|
299
|
+
n_positives: int
|
|
300
|
+
n_negatives: int
|
|
301
|
+
precision: Optional[float]
|
|
302
|
+
recall: Optional[float]
|
|
303
|
+
f1: Optional[float]
|
|
304
|
+
n_errors: int
|
|
305
|
+
n_exhausted: int
|
|
306
|
+
errors: Tuple[dict, ...] = ()
|
|
307
|
+
exhausted: Tuple[dict, ...] = ()
|
|
308
|
+
|
|
309
|
+
def to_dict(self) -> dict:
|
|
310
|
+
return {
|
|
311
|
+
"tp": self.tp, "fp": self.fp, "fn": self.fn, "tn": self.tn,
|
|
312
|
+
"n_positives": self.n_positives, "n_negatives": self.n_negatives,
|
|
313
|
+
"precision": self.precision, "recall": self.recall, "f1": self.f1,
|
|
314
|
+
"n_errors": self.n_errors, "n_exhausted": self.n_exhausted,
|
|
315
|
+
"errors": list(self.errors), "exhausted": list(self.exhausted),
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
#: Sentinel cached in place of a :class:`FiniteStructure` when
|
|
320
|
+
#: :func:`unicode_logic_kit.chem.mol_to_structure` refused a SMILES (invalid
|
|
321
|
+
#: syntax, unsupported element/bond, ...) -- see :func:`_structure_for`.
|
|
322
|
+
#: Caching the failure too (not just successes) means a SMILES that fails once
|
|
323
|
+
#: is never re-run through RDKit on a later call sharing the same
|
|
324
|
+
#: ``structure_cache``, the same cost argument the module docstring makes for
|
|
325
|
+
#: successes.
|
|
326
|
+
#:
|
|
327
|
+
#: ALIASED, not redefined: a campaign shares one
|
|
328
|
+
#: :class:`~unicode_logic_kit.chem.cache.StructureCache` between this module and
|
|
329
|
+
#: :mod:`unicode_logic_kit.eval.chem_batch`, and two structurally identical
|
|
330
|
+
#: sentinel classes would make each module's ``isinstance`` check silently
|
|
331
|
+
#: miss the other's cached failures -- reporting them as structures and
|
|
332
|
+
#: crashing in the evaluator instead.
|
|
333
|
+
_StructureBuildError = StructureBuildError
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
#: :func:`_structure_for`'s cache key -- see that function's own docstring
|
|
337
|
+
#: for why it is NOT just the bare SMILES string. Identical to
|
|
338
|
+
#: :data:`unicode_logic_kit.chem.cache.CacheKey`, so a
|
|
339
|
+
#: :class:`~unicode_logic_kit.chem.cache.StructureCache` can be handed straight
|
|
340
|
+
#: to :func:`score_definition`'s ``structure_cache``.
|
|
341
|
+
_CacheKey = Tuple[str, str, bool, bool]
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _structure_for(
|
|
345
|
+
smiles: str, cache: MutableMapping[_CacheKey, object], *,
|
|
346
|
+
naming: str = "chemlog", aromatic: bool, computed: bool,
|
|
347
|
+
) -> Union[FiniteStructure, _StructureBuildError]:
|
|
348
|
+
"""``cache[(smiles, naming, aromatic, computed)]``, building and inserting
|
|
349
|
+
it first if absent.
|
|
350
|
+
|
|
351
|
+
The cache key is the FULL triple, not the bare SMILES string, because
|
|
352
|
+
``aromatic``/``computed`` are STRUCTURE-DETERMINING parameters, not
|
|
353
|
+
incidental ones: :func:`unicode_logic_kit.chem.mol_to_structure` builds a
|
|
354
|
+
genuinely different :class:`FiniteStructure` for the same SMILES
|
|
355
|
+
depending on either (``aromatic=False`` Kekulizes the bond typing
|
|
356
|
+
instead of keeping ``bAROMATIC``; ``computed=False`` omits the five
|
|
357
|
+
ring/aromaticity/connectivity predicates entirely -- see that
|
|
358
|
+
function's own module docstring). Keying on the bare SMILES alone would
|
|
359
|
+
let one call's ``aromatic=True`` structure silently answer a LATER
|
|
360
|
+
call's ``aromatic=False`` request for the identical molecule whenever
|
|
361
|
+
the two calls share a ``structure_cache`` -- a real, reproduced bug:
|
|
362
|
+
two :func:`score_definition` calls sharing one cache, one with
|
|
363
|
+
``aromatic=True`` (checking ``bAROMATIC`` on benzene, correctly True)
|
|
364
|
+
and a second with ``aromatic=False`` on the same SMILES (which should
|
|
365
|
+
make ``bAROMATIC`` empty -- Kekulized bond typing has none), returned
|
|
366
|
+
the FIRST call's stale ``aromatic=True`` structure to the second before
|
|
367
|
+
this fix, silently reporting the wrong verdict rather than raising or
|
|
368
|
+
rebuilding.
|
|
369
|
+
|
|
370
|
+
``naming`` is the third structure-determining parameter and is in the key
|
|
371
|
+
for the same reason, even though this module always passes
|
|
372
|
+
``"chemlog"``: every formula path here ends up in ChemLog spelling
|
|
373
|
+
(:func:`unicode_logic_kit.chem.parse_chemlog_tptp` renames back to it, and
|
|
374
|
+
the ``dialect="unicode"`` path applies
|
|
375
|
+
:func:`unicode_logic_kit.chem.to_chemlog_names` explicitly -- see
|
|
376
|
+
:func:`_resolve_formula`), so the structure side has no reason to diverge
|
|
377
|
+
from it here. It is in the key anyway because the cache is no longer
|
|
378
|
+
private to this module: :class:`unicode_logic_kit.chem.cache.StructureCache`
|
|
379
|
+
is shared across a whole campaign, and other entry points DO expose
|
|
380
|
+
``naming`` (``mcp.chem_tools.molecule_to_structure``). Leaving naming out
|
|
381
|
+
kept this module correct only by an invariant nothing enforced -- the
|
|
382
|
+
moment one cache serves both, a ``naming="paper"`` request would be
|
|
383
|
+
answered with a ChemLog-spelled structure and every predicate would come
|
|
384
|
+
back uninterpreted.
|
|
385
|
+
|
|
386
|
+
Only :class:`ValueError` from ``mol_to_structure`` (a bad/unsupported
|
|
387
|
+
molecule) is caught and turned into a cached
|
|
388
|
+
:class:`_StructureBuildError` -- :class:`ImportError` (RDKit not
|
|
389
|
+
installed) and :class:`TypeError` (a caller passing something that is
|
|
390
|
+
not even a string) are environment/caller bugs, not a per-molecule data
|
|
391
|
+
problem, and are left to propagate immediately rather than being
|
|
392
|
+
reported as one identical "error" per molecule in the corpus.
|
|
393
|
+
"""
|
|
394
|
+
key: _CacheKey = (smiles, naming, aromatic, computed)
|
|
395
|
+
if key in cache:
|
|
396
|
+
return cache[key]
|
|
397
|
+
try:
|
|
398
|
+
structure = mol_to_structure(
|
|
399
|
+
smiles, naming=naming, aromatic=aromatic, computed=computed)
|
|
400
|
+
except ValueError as exc:
|
|
401
|
+
result: Union[FiniteStructure, _StructureBuildError] = (
|
|
402
|
+
_StructureBuildError(f"{type(exc).__name__}: {exc}"))
|
|
403
|
+
else:
|
|
404
|
+
result = structure
|
|
405
|
+
cache[key] = result
|
|
406
|
+
return result
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _resolve_formula(formula: Union[Node, str], dialect: str) -> Node:
|
|
410
|
+
"""``formula`` as a :class:`Node`, in ChemLog predicate spelling.
|
|
411
|
+
|
|
412
|
+
``formula`` already a :class:`Node` is returned UNCHANGED -- this
|
|
413
|
+
function trusts the caller to have it in the right vocabulary already
|
|
414
|
+
(typically because it came from :func:`unicode_logic_kit.chem.
|
|
415
|
+
parse_chemlog_tptp` itself, or from :func:`_resolve_formula`'s own
|
|
416
|
+
``dialect="unicode"`` branch on an earlier call).
|
|
417
|
+
|
|
418
|
+
``dialect="tptp"`` (the default -- this is the format an LLM asked for
|
|
419
|
+
FOL emits, and the format ChemLog's own axiom files use):
|
|
420
|
+
parsed via :func:`unicode_logic_kit.chem.parse_chemlog_tptp`, which
|
|
421
|
+
already renames the chemical vocabulary back to ChemLog's lower-case
|
|
422
|
+
spelling (bare TPTP capitalises every predicate on import -- see that
|
|
423
|
+
function's own docstring) and applies the LLM-syntax repair layer.
|
|
424
|
+
|
|
425
|
+
``dialect="unicode"``: parsed via :func:`unicode_logic_kit.api.parse_any`
|
|
426
|
+
(this kit's own ∃/∧/... surface syntax, predicates conventionally
|
|
427
|
+
CAPITALISED, e.g. ``"∃x (C(x) ∧ O(x))"``), then explicitly renamed to
|
|
428
|
+
ChemLog spelling with :func:`unicode_logic_kit.chem.to_chemlog_names` --
|
|
429
|
+
without that second step a kit-syntax formula's ``C(x)``/``O(x)`` would
|
|
430
|
+
never match a structure's stored ``c``/``o`` predicates and every
|
|
431
|
+
molecule would fail with :class:`~unicode_logic_kit.semantics.model_eval.
|
|
432
|
+
UninterpretedSymbol`, which is a genuinely different failure than "this
|
|
433
|
+
formula does not actually hold" and would be a confusing default.
|
|
434
|
+
|
|
435
|
+
Raises:
|
|
436
|
+
TypeError: ``formula`` is neither a :class:`Node` nor a ``str``.
|
|
437
|
+
ValueError: ``dialect`` is neither ``"tptp"`` nor ``"unicode"``; OR
|
|
438
|
+
the text does not parse AT ALL under the chosen dialect -- this
|
|
439
|
+
is a HARD, IMMEDIATE failure (there is nothing any molecule
|
|
440
|
+
could be evaluated against), deliberately NOT reported as a
|
|
441
|
+
per-molecule error the way an :class:`~unicode_logic_kit.
|
|
442
|
+
semantics.model_eval.UninterpretedSymbol` (formula parses fine,
|
|
443
|
+
just does not match this structure's vocabulary) is -- see
|
|
444
|
+
:func:`score_definition`'s docstring for that distinction. Note:
|
|
445
|
+
:func:`unicode_logic_kit.chem.parse_chemlog_tptp` itself actually
|
|
446
|
+
raises :class:`~unicode_logic_kit.fol.tptp_input.TptpParsingError`
|
|
447
|
+
for a genuine syntax error (its own docstring's ``ValueError``
|
|
448
|
+
claim does not match this kit version's actual behaviour, verified
|
|
449
|
+
directly here) -- caught and re-raised as ``ValueError`` below so
|
|
450
|
+
this function keeps ONE stable exception contract regardless of
|
|
451
|
+
which dialect the caller chose.
|
|
452
|
+
"""
|
|
453
|
+
if isinstance(formula, Node):
|
|
454
|
+
return formula
|
|
455
|
+
if not isinstance(formula, str):
|
|
456
|
+
raise TypeError(
|
|
457
|
+
"c3po.score_definition: formula must be a Node or str, got "
|
|
458
|
+
f"{type(formula).__name__}")
|
|
459
|
+
|
|
460
|
+
if dialect == "tptp":
|
|
461
|
+
try:
|
|
462
|
+
return parse_chemlog_tptp(formula)
|
|
463
|
+
except (TptpParsingError, ValueError) as exc:
|
|
464
|
+
raise ValueError(
|
|
465
|
+
"c3po.score_definition: formula did not parse under "
|
|
466
|
+
f"dialect='tptp': {exc}") from exc
|
|
467
|
+
|
|
468
|
+
if dialect == "unicode":
|
|
469
|
+
from ... import api # lazy: avoid import cycle
|
|
470
|
+
|
|
471
|
+
parsed = api.parse_any(formula)
|
|
472
|
+
if not parsed.ok:
|
|
473
|
+
detail = parsed.errors[-1]["message"] if parsed.errors else "unparseable"
|
|
474
|
+
raise ValueError(
|
|
475
|
+
"c3po.score_definition: formula did not parse under "
|
|
476
|
+
f"dialect='unicode' ({detail}): {formula!r}")
|
|
477
|
+
return to_chemlog_names(parsed.formula)
|
|
478
|
+
|
|
479
|
+
raise ValueError(
|
|
480
|
+
f"c3po.score_definition: dialect must be 'tptp' or 'unicode', got "
|
|
481
|
+
f"{dialect!r}")
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _classify(
|
|
485
|
+
smiles_list: Iterable[str], split_name: str, formula: Node, *,
|
|
486
|
+
all_different: bool, budget: Optional[int], aromatic: bool, computed: bool,
|
|
487
|
+
structure_cache: MutableMapping[_CacheKey, object],
|
|
488
|
+
errors: List[dict], exhausted: List[dict],
|
|
489
|
+
) -> Tuple[int, int]:
|
|
490
|
+
"""Evaluate ``formula`` against every molecule in ``smiles_list``.
|
|
491
|
+
|
|
492
|
+
Returns ``(n_holds, n_fails)`` -- how many molecules ``formula`` held /
|
|
493
|
+
did not hold on, EXCLUDING anything routed into ``errors``/``exhausted``
|
|
494
|
+
(appended to in place). The caller reinterprets ``(n_holds, n_fails)``
|
|
495
|
+
as ``(tp, fn)`` for the positive split or ``(fp, tn)`` for the negative
|
|
496
|
+
one -- see :func:`score_definition`.
|
|
497
|
+
"""
|
|
498
|
+
n_holds = 0
|
|
499
|
+
n_fails = 0
|
|
500
|
+
for smiles in smiles_list:
|
|
501
|
+
structure = _structure_for(
|
|
502
|
+
smiles, structure_cache, aromatic=aromatic, computed=computed)
|
|
503
|
+
if isinstance(structure, _StructureBuildError):
|
|
504
|
+
errors.append({
|
|
505
|
+
"smiles": smiles, "split": split_name, "stage": "structure",
|
|
506
|
+
"kind": "StructureBuildError", "message": structure.message,
|
|
507
|
+
})
|
|
508
|
+
continue
|
|
509
|
+
try:
|
|
510
|
+
result = evaluate_detailed(
|
|
511
|
+
formula, structure, all_different=all_different, budget=budget)
|
|
512
|
+
except (UninterpretedSymbol, UnsupportedNode, ValueError) as exc:
|
|
513
|
+
errors.append({
|
|
514
|
+
"smiles": smiles, "split": split_name, "stage": "evaluate",
|
|
515
|
+
"kind": type(exc).__name__, "message": str(exc),
|
|
516
|
+
})
|
|
517
|
+
continue
|
|
518
|
+
if result.exhausted:
|
|
519
|
+
exhausted.append({
|
|
520
|
+
"smiles": smiles, "split": split_name, "steps": result.steps,
|
|
521
|
+
})
|
|
522
|
+
continue
|
|
523
|
+
if result.holds:
|
|
524
|
+
n_holds += 1
|
|
525
|
+
else:
|
|
526
|
+
n_fails += 1
|
|
527
|
+
return n_holds, n_fails
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _safe_div(numerator: int, denominator: int) -> Optional[float]:
|
|
531
|
+
return None if denominator == 0 else numerator / denominator
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _f1_of(precision: Optional[float], recall: Optional[float]) -> Optional[float]:
|
|
535
|
+
if precision is None or recall is None:
|
|
536
|
+
return None
|
|
537
|
+
if precision + recall == 0:
|
|
538
|
+
return 0.0
|
|
539
|
+
return 2 * precision * recall / (precision + recall)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def score_definition(
|
|
543
|
+
formula: Union[Node, str],
|
|
544
|
+
positives: Iterable[str],
|
|
545
|
+
negatives: Iterable[str],
|
|
546
|
+
*,
|
|
547
|
+
dialect: str = "tptp",
|
|
548
|
+
all_different: bool = True,
|
|
549
|
+
budget: Optional[int] = None,
|
|
550
|
+
aromatic: bool = True,
|
|
551
|
+
computed: bool = True,
|
|
552
|
+
structure_cache: Optional[MutableMapping[_CacheKey, object]] = None,
|
|
553
|
+
) -> DefinitionScore:
|
|
554
|
+
"""Model-check ``formula`` against a positive/negative SMILES split.
|
|
555
|
+
|
|
556
|
+
This is C3PO-style evaluation: ``formula`` is a CLOSED FOL sentence over
|
|
557
|
+
the ChemLog vocabulary (typically the right-hand side of a ChEBI class's
|
|
558
|
+
``<=>`` definition -- e.g. the ``?[A1,A2,A3]: (...)`` half of a
|
|
559
|
+
``carboxylicAcid`` definition, NOT the biconditional itself: the
|
|
560
|
+
left-hand class name is a bare 0-ary atom this module's structures do
|
|
561
|
+
not interpret, so evaluating the whole biconditional would raise
|
|
562
|
+
:class:`~unicode_logic_kit.semantics.model_eval.UninterpretedSymbol` on
|
|
563
|
+
every molecule). It is evaluated ONCE PER MOLECULE, each against its OWN
|
|
564
|
+
:func:`~unicode_logic_kit.chem.mol_to_structure` structure -- there is no
|
|
565
|
+
cross-molecule comparison; "classifying" a molecule means deciding
|
|
566
|
+
whether ``formula`` is true in that ONE molecule's structure.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
formula: a parsed :class:`~unicode_logic_kit.fol.nodes.Node`, or TPTP/
|
|
570
|
+
kit-unicode text (see ``dialect=``) -- resolved ONCE, before the
|
|
571
|
+
per-molecule loop (see :func:`_resolve_formula`).
|
|
572
|
+
positives: SMILES of molecules that SHOULD satisfy ``formula`` (the
|
|
573
|
+
gold-positive set -- typically a C3PO example's
|
|
574
|
+
``meta["positive_smiles"]``).
|
|
575
|
+
negatives: SMILES of molecules that should NOT (the gold-negative
|
|
576
|
+
set -- this module does not source it for you; see the module
|
|
577
|
+
docstring's ``structures.csv`` note for why).
|
|
578
|
+
dialect: ``"tptp"`` (default) or ``"unicode"`` -- see
|
|
579
|
+
:func:`_resolve_formula`. Ignored if ``formula`` is already a
|
|
580
|
+
:class:`Node`.
|
|
581
|
+
all_different: forwarded to
|
|
582
|
+
:func:`~unicode_logic_kit.semantics.model_eval.evaluate_detailed`.
|
|
583
|
+
Defaults to ``True`` -- NOT this evaluator's own library default
|
|
584
|
+
(which is ``False``, plain FOL semantics) -- because ChemLog's
|
|
585
|
+
own TPTP convention (documented in ``model_eval``'s module
|
|
586
|
+
docstring) is that separately-quantified existentials denote
|
|
587
|
+
PAIRWISE DISTINCT individuals with no explicit ``≠`` ever
|
|
588
|
+
written, and ``dialect="tptp"`` formulas are exactly that
|
|
589
|
+
convention's own output. Pass ``all_different=False`` explicitly
|
|
590
|
+
for a formula that does NOT follow it (e.g. a hand-written
|
|
591
|
+
``dialect="unicode"`` formula using genuine plain-FOL semantics).
|
|
592
|
+
budget: forwarded to ``evaluate_detailed`` as its per-molecule step
|
|
593
|
+
budget. ``None`` (default) means unlimited -- ``exhausted`` will
|
|
594
|
+
then always be empty.
|
|
595
|
+
aromatic / computed: forwarded to
|
|
596
|
+
:func:`~unicode_logic_kit.chem.mol_to_structure` for every
|
|
597
|
+
molecule this call builds a structure for.
|
|
598
|
+
structure_cache: a caller-owned, mutable
|
|
599
|
+
``{(smiles, aromatic, computed): structure}`` dict, extended IN
|
|
600
|
+
PLACE -- the key is the FULL triple, not the bare SMILES, because
|
|
601
|
+
``aromatic``/``computed`` are structure-determining parameters
|
|
602
|
+
(see :func:`_structure_for`'s own docstring): a SMILES cached
|
|
603
|
+
under one ``aromatic=``/``computed=`` combination is never
|
|
604
|
+
returned for a call using a DIFFERENT combination, even when both
|
|
605
|
+
calls share this same dict, so passing one shared cache to calls
|
|
606
|
+
that deliberately vary ``aromatic=``/``computed=`` is always
|
|
607
|
+
safe. Pass the SAME dict across multiple :func:`score_definition`
|
|
608
|
+
calls that share (even partially overlapping) molecule sets AND
|
|
609
|
+
the same ``aromatic=``/``computed=`` choice -- e.g. scoring
|
|
610
|
+
several candidate definitions against one C3PO class's
|
|
611
|
+
positives, or scoring one definition against many classes that
|
|
612
|
+
share common negatives -- to build each distinct SMILES's
|
|
613
|
+
structure ONCE, not once per call (RDKit parsing + the
|
|
614
|
+
ring/fragment computed-predicate pass is the dominant
|
|
615
|
+
per-molecule cost). ``None`` (default) creates a fresh,
|
|
616
|
+
call-local dict -- structures are still deduplicated WITHIN one
|
|
617
|
+
call (e.g. a SMILES appearing in both ``positives`` and
|
|
618
|
+
``negatives``, or repeated in one list), just not reused across
|
|
619
|
+
separate calls.
|
|
620
|
+
|
|
621
|
+
Returns:
|
|
622
|
+
A :class:`DefinitionScore`. See its docstring for exactly how the
|
|
623
|
+
four confusion-matrix cells, the two honest failure categories, and
|
|
624
|
+
the derived precision/recall/F1 relate.
|
|
625
|
+
|
|
626
|
+
Raises:
|
|
627
|
+
TypeError: ``formula`` is neither a :class:`Node` nor ``str``.
|
|
628
|
+
ValueError: ``dialect`` is invalid, or ``formula`` (as text) fails to
|
|
629
|
+
parse AT ALL -- see :func:`_resolve_formula`. This is the ONE
|
|
630
|
+
failure mode NOT absorbed into ``DefinitionScore.errors``: a
|
|
631
|
+
formula that never became a valid AST cannot meaningfully be
|
|
632
|
+
"scored" as 0-for-everything, so this raises immediately instead
|
|
633
|
+
of manufacturing a degenerate result the caller might not
|
|
634
|
+
scrutinise. A formula that DOES parse but names a predicate this
|
|
635
|
+
vocabulary does not have (e.g. a typo, or a genuinely unknown
|
|
636
|
+
predicate) is different: it fails identically on every molecule,
|
|
637
|
+
but via the ordinary per-molecule ``UninterpretedSymbol`` path,
|
|
638
|
+
so it shows up as ``n_errors == n_positives + n_negatives`` in an
|
|
639
|
+
otherwise normally-returned score -- exactly the distinction
|
|
640
|
+
that matters here: an unusable vocabulary is reported as its own
|
|
641
|
+
error category, never as "everything came out negative".
|
|
642
|
+
ImportError: RDKit is not installed (propagated from the first
|
|
643
|
+
:func:`~unicode_logic_kit.chem.mol_to_structure` call).
|
|
644
|
+
"""
|
|
645
|
+
resolved = _resolve_formula(formula, dialect)
|
|
646
|
+
cache: MutableMapping[_CacheKey, object] = (
|
|
647
|
+
{} if structure_cache is None else structure_cache)
|
|
648
|
+
|
|
649
|
+
positives_list = list(positives)
|
|
650
|
+
negatives_list = list(negatives)
|
|
651
|
+
|
|
652
|
+
errors: List[dict] = []
|
|
653
|
+
exhausted: List[dict] = []
|
|
654
|
+
|
|
655
|
+
tp, fn = _classify(
|
|
656
|
+
positives_list, "positive", resolved,
|
|
657
|
+
all_different=all_different, budget=budget,
|
|
658
|
+
aromatic=aromatic, computed=computed,
|
|
659
|
+
structure_cache=cache, errors=errors, exhausted=exhausted,
|
|
660
|
+
)
|
|
661
|
+
fp, tn = _classify(
|
|
662
|
+
negatives_list, "negative", resolved,
|
|
663
|
+
all_different=all_different, budget=budget,
|
|
664
|
+
aromatic=aromatic, computed=computed,
|
|
665
|
+
structure_cache=cache, errors=errors, exhausted=exhausted,
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
precision = _safe_div(tp, tp + fp)
|
|
669
|
+
recall = _safe_div(tp, tp + fn)
|
|
670
|
+
f1 = _f1_of(precision, recall)
|
|
671
|
+
|
|
672
|
+
return DefinitionScore(
|
|
673
|
+
tp=tp, fp=fp, fn=fn, tn=tn,
|
|
674
|
+
n_positives=len(positives_list), n_negatives=len(negatives_list),
|
|
675
|
+
precision=precision, recall=recall, f1=f1,
|
|
676
|
+
n_errors=len(errors), n_exhausted=len(exhausted),
|
|
677
|
+
errors=tuple(errors), exhausted=tuple(exhausted),
|
|
678
|
+
)
|