Skip to content

Core

The graph data model: nodes, relations, credences, and their JSON form.

Beta dataclass

A Beta(alpha, beta) distribution over the probability.

Attributes:

Name Type Description
alpha float

First shape parameter, alpha > 0.

beta float

Second shape parameter, beta > 0.

Source code in src/credencegraph/core/credence.py
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
@dataclass(frozen=True, slots=True)
class Beta:
    """A Beta(alpha, beta) distribution over the probability.

    Attributes:
        alpha: First shape parameter, ``alpha > 0``.
        beta: Second shape parameter, ``beta > 0``.
    """

    alpha: float
    beta: float

    def __post_init__(self) -> None:
        """Validate and normalise both shape parameters to floats."""
        alpha = _real(self.alpha, "Beta.alpha")
        beta = _real(self.beta, "Beta.beta")
        if alpha <= 0.0:
            msg = f"Beta.alpha must be > 0, got {alpha!r}"
            raise ValidationError(msg)
        if beta <= 0.0:
            msg = f"Beta.beta must be > 0, got {beta!r}"
            raise ValidationError(msg)
        object.__setattr__(self, "alpha", alpha)
        object.__setattr__(self, "beta", beta)

    @property
    def concentration(self) -> float:
        """The concentration ``alpha + beta``."""
        return self.alpha + self.beta

    @property
    def mean(self) -> float:
        """The mean ``alpha / (alpha + beta)``.

        When ``alpha + beta`` overflows to infinity, both parameters are halved first. Halving
        a float this large is exact, so the ratio is unchanged and the mean is computed
        correctly instead of collapsing to zero.
        """
        total = self.concentration
        if math.isinf(total):
            half_alpha = self.alpha / 2.0
            return half_alpha / (half_alpha + self.beta / 2.0)
        return self.alpha / total

    @property
    def variance(self) -> float:
        """The variance ``mean * (1 - mean) / (alpha + beta + 1)``."""
        return self.mean * (1.0 - self.mean) / (self.concentration + 1.0)

    @classmethod
    def from_mean_concentration(cls, mean: float, concentration: float) -> Self:
        """Build a Beta from its mean and concentration ``alpha + beta``.

        Args:
            mean: The mean, strictly between 0 and 1.
            concentration: The concentration, greater than 0.

        Returns:
            The Beta with ``alpha = mean * concentration`` and ``beta = (1 - mean) * concentration``.

        Raises:
            ValidationError: If an argument is out of range.
        """
        m = _real(mean, "mean")
        k = _real(concentration, "concentration")
        if not 0.0 < m < 1.0:
            msg = f"mean must lie strictly between 0 and 1, got {m!r}"
            raise ValidationError(msg)
        if k <= 0.0:
            msg = f"concentration must be > 0, got {k!r}"
            raise ValidationError(msg)
        return cls(m * k, (1.0 - m) * k)

    @classmethod
    def from_mean_sd(cls, mean: float, sd: float) -> Self:
        """Build a Beta from its mean and standard deviation.

        Args:
            mean: The mean, strictly between 0 and 1.
            sd: The standard deviation, greater than 0 and below ``sqrt(mean * (1 - mean))``,
                the largest spread any Beta with that mean can have.

        Returns:
            The Beta with the requested mean and standard deviation.

        Raises:
            ValidationError: If an argument is out of range or the pair is unattainable.
        """
        m = _real(mean, "mean")
        s = _real(sd, "sd")
        if not 0.0 < m < 1.0:
            msg = f"mean must lie strictly between 0 and 1, got {m!r}"
            raise ValidationError(msg)
        if s <= 0.0:
            msg = f"sd must be > 0 (use Point for a credence with no spread), got {s!r}"
            raise ValidationError(msg)
        ceiling = m * (1.0 - m)
        if s * s >= ceiling:
            msg = f"sd must be below sqrt(mean * (1 - mean)) = {math.sqrt(ceiling)!r} for mean {m!r}, got {s!r}"
            raise ValidationError(msg)
        return cls.from_mean_concentration(m, ceiling / (s * s) - 1.0)

concentration property

The concentration alpha + beta.

mean property

The mean alpha / (alpha + beta).

When alpha + beta overflows to infinity, both parameters are halved first. Halving a float this large is exact, so the ratio is unchanged and the mean is computed correctly instead of collapsing to zero.

variance property

The variance mean * (1 - mean) / (alpha + beta + 1).

__post_init__()

Validate and normalise both shape parameters to floats.

Source code in src/credencegraph/core/credence.py
77
78
79
80
81
82
83
84
85
86
87
88
def __post_init__(self) -> None:
    """Validate and normalise both shape parameters to floats."""
    alpha = _real(self.alpha, "Beta.alpha")
    beta = _real(self.beta, "Beta.beta")
    if alpha <= 0.0:
        msg = f"Beta.alpha must be > 0, got {alpha!r}"
        raise ValidationError(msg)
    if beta <= 0.0:
        msg = f"Beta.beta must be > 0, got {beta!r}"
        raise ValidationError(msg)
    object.__setattr__(self, "alpha", alpha)
    object.__setattr__(self, "beta", beta)

from_mean_concentration(mean, concentration) classmethod

Build a Beta from its mean and concentration alpha + beta.

Parameters:

Name Type Description Default
mean float

The mean, strictly between 0 and 1.

required
concentration float

The concentration, greater than 0.

required

Returns:

Type Description
Self

The Beta with alpha = mean * concentration and beta = (1 - mean) * concentration.

Raises:

Type Description
ValidationError

If an argument is out of range.

Source code in src/credencegraph/core/credence.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
@classmethod
def from_mean_concentration(cls, mean: float, concentration: float) -> Self:
    """Build a Beta from its mean and concentration ``alpha + beta``.

    Args:
        mean: The mean, strictly between 0 and 1.
        concentration: The concentration, greater than 0.

    Returns:
        The Beta with ``alpha = mean * concentration`` and ``beta = (1 - mean) * concentration``.

    Raises:
        ValidationError: If an argument is out of range.
    """
    m = _real(mean, "mean")
    k = _real(concentration, "concentration")
    if not 0.0 < m < 1.0:
        msg = f"mean must lie strictly between 0 and 1, got {m!r}"
        raise ValidationError(msg)
    if k <= 0.0:
        msg = f"concentration must be > 0, got {k!r}"
        raise ValidationError(msg)
    return cls(m * k, (1.0 - m) * k)

from_mean_sd(mean, sd) classmethod

Build a Beta from its mean and standard deviation.

Parameters:

Name Type Description Default
mean float

The mean, strictly between 0 and 1.

required
sd float

The standard deviation, greater than 0 and below sqrt(mean * (1 - mean)), the largest spread any Beta with that mean can have.

required

Returns:

Type Description
Self

The Beta with the requested mean and standard deviation.

Raises:

Type Description
ValidationError

If an argument is out of range or the pair is unattainable.

Source code in src/credencegraph/core/credence.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
@classmethod
def from_mean_sd(cls, mean: float, sd: float) -> Self:
    """Build a Beta from its mean and standard deviation.

    Args:
        mean: The mean, strictly between 0 and 1.
        sd: The standard deviation, greater than 0 and below ``sqrt(mean * (1 - mean))``,
            the largest spread any Beta with that mean can have.

    Returns:
        The Beta with the requested mean and standard deviation.

    Raises:
        ValidationError: If an argument is out of range or the pair is unattainable.
    """
    m = _real(mean, "mean")
    s = _real(sd, "sd")
    if not 0.0 < m < 1.0:
        msg = f"mean must lie strictly between 0 and 1, got {m!r}"
        raise ValidationError(msg)
    if s <= 0.0:
        msg = f"sd must be > 0 (use Point for a credence with no spread), got {s!r}"
        raise ValidationError(msg)
    ceiling = m * (1.0 - m)
    if s * s >= ceiling:
        msg = f"sd must be below sqrt(mean * (1 - mean)) = {math.sqrt(ceiling)!r} for mean {m!r}, got {s!r}"
        raise ValidationError(msg)
    return cls.from_mean_concentration(m, ceiling / (s * s) - 1.0)

CredenceGraphError

Bases: Exception

Base class for every error raised by credencegraph.

Source code in src/credencegraph/core/errors.py
6
7
class CredenceGraphError(Exception):
    """Base class for every error raised by credencegraph."""

CycleError

Bases: ValidationError

A relation would close a cycle among requires, supports and refutes relations.

Attributes:

Name Type Description
cycle

The node ids around the cycle, starting and ending at the same node.

relations

The ids of the relations that form the cycle, in the order they are traversed.

Source code in src/credencegraph/core/errors.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class CycleError(ValidationError):
    """A relation would close a cycle among ``requires``, ``supports`` and ``refutes`` relations.

    Attributes:
        cycle: The node ids around the cycle, starting and ending at the same node.
        relations: The ids of the relations that form the cycle, in the order they are traversed.
    """

    def __init__(self, message: str, cycle: tuple[str, ...], relations: tuple[str, ...]) -> None:
        """Store the cycle alongside the message.

        Args:
            message: Human-readable description naming the cycle.
            cycle: Node ids around the cycle, first equal to last.
            relations: Relation ids along the cycle.
        """
        super().__init__(message)
        self.cycle = cycle
        self.relations = relations

__init__(message, cycle, relations)

Store the cycle alongside the message.

Parameters:

Name Type Description Default
message str

Human-readable description naming the cycle.

required
cycle tuple[str, ...]

Node ids around the cycle, first equal to last.

required
relations tuple[str, ...]

Relation ids along the cycle.

required
Source code in src/credencegraph/core/errors.py
22
23
24
25
26
27
28
29
30
31
32
def __init__(self, message: str, cycle: tuple[str, ...], relations: tuple[str, ...]) -> None:
    """Store the cycle alongside the message.

    Args:
        message: Human-readable description naming the cycle.
        cycle: Node ids around the cycle, first equal to last.
        relations: Relation ids along the cycle.
    """
    super().__init__(message)
    self.cycle = cycle
    self.relations = relations

Discharge dataclass

How an obligation was met.

Each kind takes only its own fields: theorem for formal; script, and the optional digest and status of its recorded run, for symbolic and numerical, which also needs the expected value and the tolerance around it; anchor, with a quote, for citation; nothing for judgement.

Attributes:

Name Type Description
kind str

One of formal, symbolic, numerical, citation and judgement.

theorem str | None

The name of the theorem that proves the step.

script str | None

Path of the Python script, relative to the graph file's directory.

digest str | None

sha256: and the hex SHA-256 of the script's bytes when it was run.

status int | None

The exit status of that run; 0 means the check passed.

value float | None

The value the numerical script's printed result must lie near.

tolerance float | None

How far from value the printed result may lie, inclusive.

anchor SourceAnchor | None

The cited source and the quote from it.

Source code in src/credencegraph/core/obligation.py
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
@dataclass(frozen=True, slots=True)
class Discharge:
    """How an obligation was met.

    Each kind takes only its own fields: ``theorem`` for ``formal``; ``script``, and the optional
    ``digest`` and ``status`` of its recorded run, for ``symbolic`` and ``numerical``, which also
    needs the expected ``value`` and the ``tolerance`` around it; ``anchor``, with a quote, for
    ``citation``; nothing for ``judgement``.

    Attributes:
        kind: One of ``formal``, ``symbolic``, ``numerical``, ``citation`` and ``judgement``.
        theorem: The name of the theorem that proves the step.
        script: Path of the Python script, relative to the graph file's directory.
        digest: ``sha256:`` and the hex SHA-256 of the script's bytes when it was run.
        status: The exit status of that run; 0 means the check passed.
        value: The value the numerical script's printed result must lie near.
        tolerance: How far from ``value`` the printed result may lie, inclusive.
        anchor: The cited source and the quote from it.
    """

    kind: str
    theorem: str | None = None
    script: str | None = None
    digest: str | None = None
    status: int | None = None
    value: float | None = None
    tolerance: float | None = None
    anchor: SourceAnchor | None = None

    def __post_init__(self) -> None:
        """Validate the fields against the kind."""
        self._check_fields()
        require_text(self.theorem, "Discharge.theorem", optional=True)
        require_text(self.script, "Discharge.script", optional=True)
        if self.script is not None and not self.script.endswith(".py"):
            msg = f"Discharge.script must be a Python script ending in .py, got {self.script!r}"
            raise ValidationError(msg)
        if (self.digest is None) != (self.status is None):
            msg = "a script discharge records both the digest and the status of its run, or neither"
            raise ValidationError(msg)
        if self.digest is not None and not (isinstance(self.digest, str) and _FILE_DIGEST.fullmatch(self.digest)):
            msg = f"Discharge.digest must be 'sha256:' and 64 lowercase hex digits, got {self.digest!r}"
            raise ValidationError(msg)
        if self.status is not None and (isinstance(self.status, bool) or not isinstance(self.status, int)):
            msg = f"Discharge.status must be an integer exit status, got {self.status!r}"
            raise ValidationError(msg)
        for name, low in (("value", -math.inf), ("tolerance", 0.0)):
            number = getattr(self, name)
            if number is None:
                continue
            if isinstance(number, bool) or not isinstance(number, int | float) or not low < number < math.inf:
                bound = "a finite number" if low == -math.inf else "a finite number above 0"
                msg = f"Discharge.{name} must be {bound}, got {number!r}"
                raise ValidationError(msg)
            object.__setattr__(self, name, float(number))
        if self.anchor is not None:
            if not isinstance(self.anchor, SourceAnchor):
                msg = f"Discharge.anchor must be a SourceAnchor, got {self.anchor!r}"
                raise ValidationError(msg)
            if self.anchor.quote is None:
                msg = "a citation discharge needs an anchor with a quote"
                raise ValidationError(msg)

    def _check_fields(self) -> None:
        """Check that the kind is known and that exactly its own fields are given."""
        if self.kind not in DISCHARGE_KINDS:
            msg = f"Discharge kind must be one of {', '.join(DISCHARGE_KINDS)}, got {self.kind!r}"
            raise ValidationError(msg)
        given = {name for name in _FIELDS if getattr(self, name) is not None}
        missing = sorted(_REQUIRED[self.kind] - given)
        if missing:
            msg = f"a {self.kind} discharge needs {', '.join(missing)}"
            raise ValidationError(msg)
        extra = sorted(given - _REQUIRED[self.kind] - _OPTIONAL.get(self.kind, set()))
        if extra:
            msg = f"a {self.kind} discharge takes no {', '.join(extra)}"
            raise ValidationError(msg)

__post_init__()

Validate the fields against the kind.

Source code in src/credencegraph/core/obligation.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def __post_init__(self) -> None:
    """Validate the fields against the kind."""
    self._check_fields()
    require_text(self.theorem, "Discharge.theorem", optional=True)
    require_text(self.script, "Discharge.script", optional=True)
    if self.script is not None and not self.script.endswith(".py"):
        msg = f"Discharge.script must be a Python script ending in .py, got {self.script!r}"
        raise ValidationError(msg)
    if (self.digest is None) != (self.status is None):
        msg = "a script discharge records both the digest and the status of its run, or neither"
        raise ValidationError(msg)
    if self.digest is not None and not (isinstance(self.digest, str) and _FILE_DIGEST.fullmatch(self.digest)):
        msg = f"Discharge.digest must be 'sha256:' and 64 lowercase hex digits, got {self.digest!r}"
        raise ValidationError(msg)
    if self.status is not None and (isinstance(self.status, bool) or not isinstance(self.status, int)):
        msg = f"Discharge.status must be an integer exit status, got {self.status!r}"
        raise ValidationError(msg)
    for name, low in (("value", -math.inf), ("tolerance", 0.0)):
        number = getattr(self, name)
        if number is None:
            continue
        if isinstance(number, bool) or not isinstance(number, int | float) or not low < number < math.inf:
            bound = "a finite number" if low == -math.inf else "a finite number above 0"
            msg = f"Discharge.{name} must be {bound}, got {number!r}"
            raise ValidationError(msg)
        object.__setattr__(self, name, float(number))
    if self.anchor is not None:
        if not isinstance(self.anchor, SourceAnchor):
            msg = f"Discharge.anchor must be a SourceAnchor, got {self.anchor!r}"
            raise ValidationError(msg)
        if self.anchor.quote is None:
            msg = "a citation discharge needs an anchor with a quote"
            raise ValidationError(msg)

Graph

Nodes and relations keyed by stable string ids.

Every write is validated and leaves the graph unchanged when it is rejected. Relations must name existing nodes, and the subgraph of requires, supports and refutes relations must stay acyclic. Annotation relations and equivalent/exclusive relations do not take part in the cycle check.

Source code in src/credencegraph/core/graph.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
class Graph:
    """Nodes and relations keyed by stable string ids.

    Every write is validated and leaves the graph unchanged when it is rejected. Relations must name
    existing nodes, and the subgraph of ``requires``, ``supports`` and ``refutes`` relations must
    stay acyclic. Annotation relations and ``equivalent``/``exclusive`` relations do not take part in
    the cycle check.
    """

    def __init__(self) -> None:
        """Create an empty graph."""
        self._nodes: dict[str, Node] = {}
        self._relations: dict[str, Relation] = {}
        # node id -> [(relation id, relation type, target id)], for the acyclic subgraph only
        self._acyclic_out: dict[str, list[tuple[str, str, str]]] = {}

    @property
    def nodes(self) -> Mapping[str, Node]:
        """Read-only view of the nodes by id, in insertion order."""
        return MappingProxyType(self._nodes)

    @property
    def relations(self) -> Mapping[str, Relation]:
        """Read-only view of the relations by id, in insertion order."""
        return MappingProxyType(self._relations)

    def __len__(self) -> int:
        """Return the number of nodes."""
        return len(self._nodes)

    def __iter__(self) -> Iterator[Node]:
        """Iterate over the nodes in insertion order."""
        return iter(self._nodes.values())

    def __contains__(self, node_id: object) -> bool:
        """Tell whether a node with this id exists."""
        return node_id in self._nodes

    def __eq__(self, other: object) -> bool:
        """Compare graphs by their nodes and relations, ignoring insertion order."""
        if not isinstance(other, Graph):
            return NotImplemented
        return self._nodes == other._nodes and self._relations == other._relations

    __hash__ = None  # type: ignore[assignment]

    def __repr__(self) -> str:
        """Summarise the graph."""
        return f"Graph(nodes={len(self._nodes)}, relations={len(self._relations)})"

    def add_node(self, node: Node) -> Node:
        """Add a node.

        Args:
            node: The node to add.

        Returns:
            The node.

        Raises:
            ValidationError: If ``node`` is not a ``Node`` or its id is already taken.
        """
        if not isinstance(node, Node):
            msg = f"add_node expects a Node, got {type(node).__name__}"
            raise ValidationError(msg)
        if node.id in self._nodes:
            msg = f"duplicate node id {node.id!r}"
            raise ValidationError(msg)
        self._nodes[node.id] = node
        return node

    def add_relation(self, relation: Relation) -> Relation:
        """Add a relation.

        Args:
            relation: The relation to add.

        Returns:
            The relation.

        Raises:
            ValidationError: If ``relation`` is not a ``Relation``, its id is already taken, or an endpoint
                is not a node of the graph.
            CycleError: If a ``requires``, ``supports`` or ``refutes`` relation would close a cycle among
                such relations. The message names the cycle.
        """
        if not isinstance(relation, Relation):
            msg = f"add_relation expects a Relation, got {type(relation).__name__}"
            raise ValidationError(msg)
        if relation.id in self._relations:
            msg = f"duplicate relation id {relation.id!r}"
            raise ValidationError(msg)
        for role, node_id in (("source", relation.source), ("target", relation.target)):
            if node_id not in self._nodes:
                msg = f"relation {relation.id!r} has {role} {node_id!r}, which is not a node of the graph"
                raise ValidationError(msg)
        if relation.type in ACYCLIC_TYPES:
            self._reject_cycle(relation)
            self._acyclic_out.setdefault(relation.source, []).append((relation.id, relation.type, relation.target))
        self._relations[relation.id] = relation
        return relation

    def _reject_cycle(self, relation: Relation) -> None:
        """Raise ``CycleError`` if adding ``relation`` would close a cycle.

        A cycle exists exactly when a path already leads from the relation's target back to its source.

        Args:
            relation: The candidate relation, of an acyclic type.

        Raises:
            CycleError: If a path leads from ``relation.target`` to ``relation.source``.
        """
        path = self._find_path(relation.target, relation.source)
        if path is None:
            return
        edges = [(relation.id, relation.type, relation.target), *path]
        nodes = [relation.source, *(target for _, _, target in edges)]
        rendered = nodes[0]
        for (rel_id, rel_type, _), node in zip(edges, nodes[1:], strict=True):
            rendered += f" --{rel_type}[{rel_id}]--> {node}"
        msg = f"relation {relation.id!r} would create a cycle: {rendered}"
        raise CycleError(msg, tuple(nodes), tuple(rel_id for rel_id, _, _ in edges))

    def _find_path(self, start: str, goal: str) -> list[tuple[str, str, str]] | None:
        """Find a path along acyclic-type relations from ``start`` to ``goal``.

        Args:
            start: Node id to start from.
            goal: Node id to reach.

        Returns:
            The edges of a path as ``(relation id, relation type, target id)``, empty when
            ``start == goal``, or ``None`` when ``goal`` is unreachable.
        """
        if start == goal:
            return []
        came_from: dict[str, tuple[str, tuple[str, str, str]]] = {}
        stack = [start]
        seen = {start}
        while stack:
            current = stack.pop()
            for edge in self._acyclic_out.get(current, ()):
                nxt = edge[2]
                if nxt in seen:
                    continue
                seen.add(nxt)
                came_from[nxt] = (current, edge)
                if nxt == goal:
                    path = []
                    node = goal
                    while node != start:
                        node, step = came_from[node][0], came_from[node][1]
                        path.append(step)
                    return path[::-1]
                stack.append(nxt)
        return None

nodes property

Read-only view of the nodes by id, in insertion order.

relations property

Read-only view of the relations by id, in insertion order.

__contains__(node_id)

Tell whether a node with this id exists.

Source code in src/credencegraph/core/graph.py
47
48
49
def __contains__(self, node_id: object) -> bool:
    """Tell whether a node with this id exists."""
    return node_id in self._nodes

__eq__(other)

Compare graphs by their nodes and relations, ignoring insertion order.

Source code in src/credencegraph/core/graph.py
51
52
53
54
55
def __eq__(self, other: object) -> bool:
    """Compare graphs by their nodes and relations, ignoring insertion order."""
    if not isinstance(other, Graph):
        return NotImplemented
    return self._nodes == other._nodes and self._relations == other._relations

__init__()

Create an empty graph.

Source code in src/credencegraph/core/graph.py
22
23
24
25
26
27
def __init__(self) -> None:
    """Create an empty graph."""
    self._nodes: dict[str, Node] = {}
    self._relations: dict[str, Relation] = {}
    # node id -> [(relation id, relation type, target id)], for the acyclic subgraph only
    self._acyclic_out: dict[str, list[tuple[str, str, str]]] = {}

__iter__()

Iterate over the nodes in insertion order.

Source code in src/credencegraph/core/graph.py
43
44
45
def __iter__(self) -> Iterator[Node]:
    """Iterate over the nodes in insertion order."""
    return iter(self._nodes.values())

__len__()

Return the number of nodes.

Source code in src/credencegraph/core/graph.py
39
40
41
def __len__(self) -> int:
    """Return the number of nodes."""
    return len(self._nodes)

__repr__()

Summarise the graph.

Source code in src/credencegraph/core/graph.py
59
60
61
def __repr__(self) -> str:
    """Summarise the graph."""
    return f"Graph(nodes={len(self._nodes)}, relations={len(self._relations)})"

add_node(node)

Add a node.

Parameters:

Name Type Description Default
node Node

The node to add.

required

Returns:

Type Description
Node

The node.

Raises:

Type Description
ValidationError

If node is not a Node or its id is already taken.

Source code in src/credencegraph/core/graph.py
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
def add_node(self, node: Node) -> Node:
    """Add a node.

    Args:
        node: The node to add.

    Returns:
        The node.

    Raises:
        ValidationError: If ``node`` is not a ``Node`` or its id is already taken.
    """
    if not isinstance(node, Node):
        msg = f"add_node expects a Node, got {type(node).__name__}"
        raise ValidationError(msg)
    if node.id in self._nodes:
        msg = f"duplicate node id {node.id!r}"
        raise ValidationError(msg)
    self._nodes[node.id] = node
    return node

add_relation(relation)

Add a relation.

Parameters:

Name Type Description Default
relation Relation

The relation to add.

required

Returns:

Type Description
Relation

The relation.

Raises:

Type Description
ValidationError

If relation is not a Relation, its id is already taken, or an endpoint is not a node of the graph.

CycleError

If a requires, supports or refutes relation would close a cycle among such relations. The message names the cycle.

Source code in src/credencegraph/core/graph.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def add_relation(self, relation: Relation) -> Relation:
    """Add a relation.

    Args:
        relation: The relation to add.

    Returns:
        The relation.

    Raises:
        ValidationError: If ``relation`` is not a ``Relation``, its id is already taken, or an endpoint
            is not a node of the graph.
        CycleError: If a ``requires``, ``supports`` or ``refutes`` relation would close a cycle among
            such relations. The message names the cycle.
    """
    if not isinstance(relation, Relation):
        msg = f"add_relation expects a Relation, got {type(relation).__name__}"
        raise ValidationError(msg)
    if relation.id in self._relations:
        msg = f"duplicate relation id {relation.id!r}"
        raise ValidationError(msg)
    for role, node_id in (("source", relation.source), ("target", relation.target)):
        if node_id not in self._nodes:
            msg = f"relation {relation.id!r} has {role} {node_id!r}, which is not a node of the graph"
            raise ValidationError(msg)
    if relation.type in ACYCLIC_TYPES:
        self._reject_cycle(relation)
        self._acyclic_out.setdefault(relation.source, []).append((relation.id, relation.type, relation.target))
    self._relations[relation.id] = relation
    return relation

Node dataclass

A node of a graph: usually a proposition, but any carried entity.

base and stated accept a bare float, read as a Point. sources accepts any iterable of anchors. attributes is deep-copied and frozen on construction.

Attributes:

Name Type Description
id str

Stable identifier, unique within the graph.

kind str

Free label the engine attaches no meaning to.

statement str | None

The normalised proposition.

sources tuple[SourceAnchor, ...]

Where the proposition came from.

base Credence | None

For a node with no inferential parents, its prior credence.

stated Credence | None

The credence the source itself asserts, used only by diagnostics.

attributes Mapping[str, FrozenJSON]

Open JSON metadata, stored read-only: objects as read-only mappings, arrays as tuples.

Source code in src/credencegraph/core/node.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@dataclass(frozen=True, slots=True)
class Node:
    """A node of a graph: usually a proposition, but any carried entity.

    ``base`` and ``stated`` accept a bare float, read as a ``Point``. ``sources`` accepts any iterable
    of anchors. ``attributes`` is deep-copied and frozen on construction.

    Attributes:
        id: Stable identifier, unique within the graph.
        kind: Free label the engine attaches no meaning to.
        statement: The normalised proposition.
        sources: Where the proposition came from.
        base: For a node with no inferential parents, its prior credence.
        stated: The credence the source itself asserts, used only by diagnostics.
        attributes: Open JSON metadata, stored read-only: objects as read-only mappings, arrays as
            tuples.
    """

    id: str
    kind: str = DEFAULT_KIND
    statement: str | None = None
    sources: tuple[SourceAnchor, ...] = ()
    base: Credence | None = None
    stated: Credence | None = None
    attributes: Mapping[str, FrozenJSON] = field(default_factory=dict)

    def __post_init__(self) -> None:
        """Validate every field and normalise the flexible ones."""
        require_text(self.id, "Node.id")
        require_text(self.kind, "Node.kind")
        require_text(self.statement, f"Node {self.id!r} statement", optional=True)
        sources = self.sources
        if isinstance(sources, str | bytes) or not isinstance(sources, Iterable):
            msg = f"Node {self.id!r} sources must be a sequence of SourceAnchor, got {sources!r}"
            raise ValidationError(msg)
        sources = tuple(sources)
        for anchor in sources:
            if not isinstance(anchor, SourceAnchor):
                msg = f"Node {self.id!r} sources must hold SourceAnchor objects, got {anchor!r}"
                raise ValidationError(msg)
        object.__setattr__(self, "sources", sources)
        for name in ("base", "stated"):
            value = getattr(self, name)
            if value is not None:
                object.__setattr__(self, name, as_credence(value))
        object.__setattr__(self, "attributes", freeze_attributes(self.attributes, f"Node {self.id!r} attributes"))

__post_init__()

Validate every field and normalise the flexible ones.

Source code in src/credencegraph/core/node.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def __post_init__(self) -> None:
    """Validate every field and normalise the flexible ones."""
    require_text(self.id, "Node.id")
    require_text(self.kind, "Node.kind")
    require_text(self.statement, f"Node {self.id!r} statement", optional=True)
    sources = self.sources
    if isinstance(sources, str | bytes) or not isinstance(sources, Iterable):
        msg = f"Node {self.id!r} sources must be a sequence of SourceAnchor, got {sources!r}"
        raise ValidationError(msg)
    sources = tuple(sources)
    for anchor in sources:
        if not isinstance(anchor, SourceAnchor):
            msg = f"Node {self.id!r} sources must hold SourceAnchor objects, got {anchor!r}"
            raise ValidationError(msg)
    object.__setattr__(self, "sources", sources)
    for name in ("base", "stated"):
        value = getattr(self, name)
        if value is not None:
            object.__setattr__(self, name, as_credence(value))
    object.__setattr__(self, "attributes", freeze_attributes(self.attributes, f"Node {self.id!r} attributes"))

Obligation dataclass

What must be true for an inferential step to hold, and how it was met, if it was.

Attributes:

Name Type Description
statement str

The condition, in words.

discharge Discharge | None

How it was met, or None while it is open.

Source code in src/credencegraph/core/obligation.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
@dataclass(frozen=True, slots=True)
class Obligation:
    """What must be true for an inferential step to hold, and how it was met, if it was.

    Attributes:
        statement: The condition, in words.
        discharge: How it was met, or ``None`` while it is open.
    """

    statement: str
    discharge: Discharge | None = None

    def __post_init__(self) -> None:
        """Validate the fields."""
        require_text(self.statement, "Obligation.statement")
        if self.discharge is not None and not isinstance(self.discharge, Discharge):
            msg = f"Obligation.discharge must be a Discharge, got {self.discharge!r}"
            raise ValidationError(msg)

__post_init__()

Validate the fields.

Source code in src/credencegraph/core/obligation.py
157
158
159
160
161
162
def __post_init__(self) -> None:
    """Validate the fields."""
    require_text(self.statement, "Obligation.statement")
    if self.discharge is not None and not isinstance(self.discharge, Discharge):
        msg = f"Obligation.discharge must be a Discharge, got {self.discharge!r}"
        raise ValidationError(msg)

Point dataclass

A credence with no spread: the probability is exactly p.

Attributes:

Name Type Description
p float

The probability, 0 <= p <= 1.

Source code in src/credencegraph/core/credence.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@dataclass(frozen=True, slots=True)
class Point:
    """A credence with no spread: the probability is exactly ``p``.

    Attributes:
        p: The probability, ``0 <= p <= 1``.
    """

    p: float

    def __post_init__(self) -> None:
        """Validate and normalise ``p`` to a float."""
        p = _real(self.p, "Point.p")
        if not 0.0 <= p <= 1.0:
            msg = f"Point.p must lie in [0, 1], got {p!r}"
            raise ValidationError(msg)
        object.__setattr__(self, "p", p)

    @property
    def mean(self) -> float:
        """The mean, equal to ``p``."""
        return self.p

    @property
    def variance(self) -> float:
        """The variance, always zero."""
        return 0.0

mean property

The mean, equal to p.

variance property

The variance, always zero.

__post_init__()

Validate and normalise p to a float.

Source code in src/credencegraph/core/credence.py
46
47
48
49
50
51
52
def __post_init__(self) -> None:
    """Validate and normalise ``p`` to a float."""
    p = _real(self.p, "Point.p")
    if not 0.0 <= p <= 1.0:
        msg = f"Point.p must lie in [0, 1], got {p!r}"
        raise ValidationError(msg)
    object.__setattr__(self, "p", p)

Relation dataclass

A directed relation between two nodes.

strength is required for requires, supports and refutes, and forbidden for every other type, including equivalent, exclusive and all annotations. A bare float is read as a Point. equivalent and exclusive relations may not join a node to itself. Only an inferential relation may carry an obligation.

Attributes:

Name Type Description
id str

Stable identifier, unique within the graph.

type str

An inferential type or any other string, which makes the relation an annotation.

source str

Id of the source node.

target str

Id of the target node.

strength Credence | None

The relation's strength, per the rules above.

attributes Mapping[str, FrozenJSON]

Open JSON metadata, stored read-only: objects as read-only mappings, arrays as tuples.

obligation Obligation | None

What must be true for the step to hold, and how it was met.

Source code in src/credencegraph/core/relation.py
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
@dataclass(frozen=True, slots=True)
class Relation:
    """A directed relation between two nodes.

    ``strength`` is required for ``requires``, ``supports`` and ``refutes``, and forbidden for
    every other type, including ``equivalent``, ``exclusive`` and all annotations. A bare float is read
    as a ``Point``. ``equivalent`` and ``exclusive`` relations may not join a node to itself. Only an
    inferential relation may carry an ``obligation``.

    Attributes:
        id: Stable identifier, unique within the graph.
        type: An inferential type or any other string, which makes the relation an annotation.
        source: Id of the source node.
        target: Id of the target node.
        strength: The relation's strength, per the rules above.
        attributes: Open JSON metadata, stored read-only: objects as read-only mappings, arrays as
            tuples.
        obligation: What must be true for the step to hold, and how it was met.
    """

    id: str
    type: str
    source: str
    target: str
    strength: Credence | None = None
    attributes: Mapping[str, FrozenJSON] = field(default_factory=dict)
    obligation: Obligation | None = None

    def __post_init__(self) -> None:
        """Validate the fields and enforce the strength rules."""
        require_text(self.id, "Relation.id")
        require_text(self.type, f"Relation {self.id!r} type")
        require_text(self.source, f"Relation {self.id!r} source")
        require_text(self.target, f"Relation {self.id!r} target")
        if self.type in STRENGTH_TYPES:
            if self.strength is None:
                msg = f"Relation {self.id!r} of type {self.type!r} requires a strength"
                raise ValidationError(msg)
            object.__setattr__(self, "strength", as_credence(self.strength))
        elif self.strength is not None:
            kind = "an inferential relation that carries no strength" if is_inferential(self.type) else "an annotation"
            msg = f"Relation {self.id!r} of type {self.type!r} is {kind}; it must not have a strength"
            raise ValidationError(msg)
        if self.type in (EQUIVALENT, EXCLUSIVE) and self.source == self.target:
            msg = f"Relation {self.id!r} of type {self.type!r} joins node {self.source!r} to itself"
            raise ValidationError(msg)
        object.__setattr__(self, "attributes", freeze_attributes(self.attributes, f"Relation {self.id!r} attributes"))
        if self.obligation is not None:
            if not isinstance(self.obligation, Obligation):
                msg = f"Relation {self.id!r} obligation must be an Obligation, got {self.obligation!r}"
                raise ValidationError(msg)
            if not is_inferential(self.type):
                msg = f"Relation {self.id!r} of type {self.type!r} is an annotation; it must not have an obligation"
                raise ValidationError(msg)

    @property
    def is_inferential(self) -> bool:
        """Whether the relation is inferential rather than an annotation."""
        return is_inferential(self.type)

is_inferential property

Whether the relation is inferential rather than an annotation.

__post_init__()

Validate the fields and enforce the strength rules.

Source code in src/credencegraph/core/relation.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def __post_init__(self) -> None:
    """Validate the fields and enforce the strength rules."""
    require_text(self.id, "Relation.id")
    require_text(self.type, f"Relation {self.id!r} type")
    require_text(self.source, f"Relation {self.id!r} source")
    require_text(self.target, f"Relation {self.id!r} target")
    if self.type in STRENGTH_TYPES:
        if self.strength is None:
            msg = f"Relation {self.id!r} of type {self.type!r} requires a strength"
            raise ValidationError(msg)
        object.__setattr__(self, "strength", as_credence(self.strength))
    elif self.strength is not None:
        kind = "an inferential relation that carries no strength" if is_inferential(self.type) else "an annotation"
        msg = f"Relation {self.id!r} of type {self.type!r} is {kind}; it must not have a strength"
        raise ValidationError(msg)
    if self.type in (EQUIVALENT, EXCLUSIVE) and self.source == self.target:
        msg = f"Relation {self.id!r} of type {self.type!r} joins node {self.source!r} to itself"
        raise ValidationError(msg)
    object.__setattr__(self, "attributes", freeze_attributes(self.attributes, f"Relation {self.id!r} attributes"))
    if self.obligation is not None:
        if not isinstance(self.obligation, Obligation):
            msg = f"Relation {self.id!r} obligation must be an Obligation, got {self.obligation!r}"
            raise ValidationError(msg)
        if not is_inferential(self.type):
            msg = f"Relation {self.id!r} of type {self.type!r} is an annotation; it must not have an obligation"
            raise ValidationError(msg)

SourceAnchor dataclass

Where a proposition came from.

Attributes:

Name Type Description
document str

Caller-chosen identifier of the source (a DOI, a path, a URL).

locator str | None

A page, section or character span within the document.

quote str | None

The anchored text.

digest str | None

A hash of the anchored text, for detecting later changes to the source.

Source code in src/credencegraph/core/anchor.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@dataclass(frozen=True, slots=True)
class SourceAnchor:
    """Where a proposition came from.

    Attributes:
        document: Caller-chosen identifier of the source (a DOI, a path, a URL).
        locator: A page, section or character span within the document.
        quote: The anchored text.
        digest: A hash of the anchored text, for detecting later changes to the source.
    """

    document: str
    locator: str | None = None
    quote: str | None = None
    digest: str | None = None

    def __post_init__(self) -> None:
        """Validate the fields."""
        require_text(self.document, "SourceAnchor.document")
        require_text(self.locator, "SourceAnchor.locator", optional=True)
        require_text(self.quote, "SourceAnchor.quote", optional=True)
        require_text(self.digest, "SourceAnchor.digest", optional=True)

__post_init__()

Validate the fields.

Source code in src/credencegraph/core/anchor.py
44
45
46
47
48
49
def __post_init__(self) -> None:
    """Validate the fields."""
    require_text(self.document, "SourceAnchor.document")
    require_text(self.locator, "SourceAnchor.locator", optional=True)
    require_text(self.quote, "SourceAnchor.quote", optional=True)
    require_text(self.digest, "SourceAnchor.digest", optional=True)

ValidationError

Bases: CredenceGraphError, ValueError

A value, node, relation or document breaks a rule of the data model.

Source code in src/credencegraph/core/errors.py
10
11
class ValidationError(CredenceGraphError, ValueError):
    """A value, node, relation or document breaks a rule of the data model."""

as_credence(value)

Coerce value to a credence, reading a bare float as a Point.

Parameters:

Name Type Description Default
value Credence | float

A Beta, a Point or a probability.

required

Returns:

Type Description
Credence

The credence.

Raises:

Type Description
ValidationError

If value is none of those or the probability is out of range.

Source code in src/credencegraph/core/credence.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def as_credence(value: Credence | float) -> Credence:
    """Coerce ``value`` to a credence, reading a bare float as a ``Point``.

    Args:
        value: A ``Beta``, a ``Point`` or a probability.

    Returns:
        The credence.

    Raises:
        ValidationError: If ``value`` is none of those or the probability is out of range.
    """
    if isinstance(value, Beta | Point):
        return value
    return Point(value)

dump(graph, path, *, indent=2)

Write a graph to a JSON file.

Parameters:

Name Type Description Default
graph Graph

The graph.

required
path str | Path

Destination file.

required
indent int | None

JSON indentation, or None for a single line.

2
Source code in src/credencegraph/core/serialization.py
281
282
283
284
285
286
287
288
289
def dump(graph: Graph, path: str | Path, *, indent: int | None = 2) -> None:
    """Write a graph to a JSON file.

    Args:
        graph: The graph.
        path: Destination file.
        indent: JSON indentation, or ``None`` for a single line.
    """
    Path(path).write_text(dumps(graph, indent=indent) + "\n", encoding="utf-8")

dumps(graph, *, indent=2)

Serialise a graph to a JSON string.

Parameters:

Name Type Description Default
graph Graph

The graph.

required
indent int | None

JSON indentation, or None for a single line.

2

Returns:

Type Description
str

The JSON text.

Source code in src/credencegraph/core/serialization.py
242
243
244
245
246
247
248
249
250
251
252
def dumps(graph: Graph, *, indent: int | None = 2) -> str:
    """Serialise a graph to a JSON string.

    Args:
        graph: The graph.
        indent: JSON indentation, or ``None`` for a single line.

    Returns:
        The JSON text.
    """
    return json.dumps(graph_to_dict(graph), indent=indent, allow_nan=False)

graph_from_dict(data)

Build a graph from a dictionary in the credencegraph JSON format.

Nodes are added before relations, in document order, through the same validated writes as any other graph, so a cycle in the file is rejected and named.

Parameters:

Name Type Description Default
data Any

The decoded JSON document.

required

Returns:

Type Description
Graph

The graph.

Raises:

Type Description
ValidationError

If the document is malformed, has the wrong format or an unsupported version, or breaks a rule of the data model. The message locates the offending item.

CycleError

If the relations form a cycle among requires, supports and refutes.

Source code in src/credencegraph/core/serialization.py
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
def graph_from_dict(data: Any) -> Graph:
    """Build a graph from a dictionary in the ``credencegraph`` JSON format.

    Nodes are added before relations, in document order, through the same validated writes as
    any other graph, so a cycle in the file is rejected and named.

    Args:
        data: The decoded JSON document.

    Returns:
        The graph.

    Raises:
        ValidationError: If the document is malformed, has the wrong format or an unsupported version, or
            breaks a rule of the data model. The message locates the offending item.
        CycleError: If the relations form a cycle among ``requires``, ``supports`` and ``refutes``.
    """
    document = _mapping(data, "document", _GRAPH_FIELDS, {"format", "version", "nodes", "relations"})
    if document["format"] != FORMAT:
        msg = f"document format must be {FORMAT!r}, got {document['format']!r}"
        raise ValidationError(msg)
    version = document["version"]
    if isinstance(version, bool) or version != VERSION:
        msg = f"unsupported {FORMAT} version {version!r}; this release reads version {VERSION}"
        raise ValidationError(msg)
    graph = Graph()
    for i, item in enumerate(_array(document["nodes"], "nodes")):
        path = f"nodes[{i}]"
        _build(path, lambda item=item, path=path: graph.add_node(_node_from_dict(item, path)))
    for i, item in enumerate(_array(document["relations"], "relations")):
        path = f"relations[{i}]"
        _build(path, lambda item=item, path=path: graph.add_relation(_relation_from_dict(item, path)))
    return graph

graph_to_dict(graph)

Convert a graph to a JSON-ready dictionary in the current format version.

Parameters:

Name Type Description Default
graph Graph

The graph.

required

Returns:

Type Description
dict[str, Any]

A dictionary with format, version, nodes and relations.

Source code in src/credencegraph/core/serialization.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def graph_to_dict(graph: Graph) -> dict[str, Any]:
    """Convert a graph to a JSON-ready dictionary in the current format version.

    Args:
        graph: The graph.

    Returns:
        A dictionary with ``format``, ``version``, ``nodes`` and ``relations``.
    """
    return {
        "format": FORMAT,
        "version": VERSION,
        "nodes": [_node_to_dict(node) for node in graph.nodes.values()],
        "relations": [_relation_to_dict(rel) for rel in graph.relations.values()],
    }

is_inferential(relation_type)

Tell whether relation_type is inferential rather than an annotation.

Parameters:

Name Type Description Default
relation_type str

The relation type string.

required

Returns:

Type Description
bool

True for requires, supports, refutes, equivalent and exclusive.

Source code in src/credencegraph/core/relation.py
30
31
32
33
34
35
36
37
38
39
def is_inferential(relation_type: str) -> bool:
    """Tell whether ``relation_type`` is inferential rather than an annotation.

    Args:
        relation_type: The relation type string.

    Returns:
        ``True`` for ``requires``, ``supports``, ``refutes``, ``equivalent`` and ``exclusive``.
    """
    return relation_type in INFERENTIAL_TYPES

json_schema()

Return the JSON Schema for the current format version.

The schema covers structure and types. Rules it cannot express, namely unique ids, relations naming existing nodes, acyclicity, and the ban on an equivalent or exclusive relation joining a node to itself, are enforced when a graph is loaded.

Returns:

Type Description
dict[str, Any]

The schema as a dictionary.

Source code in src/credencegraph/core/serialization.py
308
309
310
311
312
313
314
315
316
317
318
319
def json_schema() -> dict[str, Any]:
    """Return the JSON Schema for the current format version.

    The schema covers structure and types. Rules it cannot express, namely unique ids, relations
    naming existing nodes, acyclicity, and the ban on an ``equivalent`` or ``exclusive`` relation
    joining a node to itself, are enforced when a graph is loaded.

    Returns:
        The schema as a dictionary.
    """
    text = resources.files("credencegraph.core").joinpath("schema", SCHEMA_RESOURCE).read_text(encoding="utf-8")
    return json.loads(text)

load(path)

Read a graph from a JSON file.

Parameters:

Name Type Description Default
path str | Path

Source file.

required

Returns:

Type Description
Graph

The graph.

Raises:

Type Description
ValidationError

As loads.

CycleError

As loads.

Source code in src/credencegraph/core/serialization.py
292
293
294
295
296
297
298
299
300
301
302
303
304
305
def load(path: str | Path) -> Graph:
    """Read a graph from a JSON file.

    Args:
        path: Source file.

    Returns:
        The graph.

    Raises:
        ValidationError: As ``loads``.
        CycleError: As ``loads``.
    """
    return loads(Path(path).read_text(encoding="utf-8"))

loads(text)

Parse a graph from a JSON string.

Parameters:

Name Type Description Default
text str

The JSON text.

required

Returns:

Type Description
Graph

The graph.

Raises:

Type Description
ValidationError

If the text is not valid JSON, or as graph_from_dict.

CycleError

As graph_from_dict.

Source code in src/credencegraph/core/serialization.py
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
def loads(text: str) -> Graph:
    """Parse a graph from a JSON string.

    Args:
        text: The JSON text.

    Returns:
        The graph.

    Raises:
        ValidationError: If the text is not valid JSON, or as ``graph_from_dict``.
        CycleError: As ``graph_from_dict``.
    """
    try:
        data = json.loads(text, parse_constant=_reject_constant)
    except json.JSONDecodeError as exc:
        msg = f"not valid JSON: {exc}"
        raise ValidationError(msg) from exc
    return graph_from_dict(data)

parse_credence(text)

Parse a credence written as text: a bare probability is a Point, beta:A,B a Beta.

This is the syntax of the command line and of rubric files.

Parameters:

Name Type Description Default
text str

The text, such as 0.3 or beta:8,2.

required

Returns:

Type Description
Credence

The credence.

Raises:

Type Description
ValidationError

If the text is neither form, or its numbers are out of range. The message starts with the text, quoted.

Source code in src/credencegraph/core/credence.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
def parse_credence(text: str) -> Credence:
    """Parse a credence written as text: a bare probability is a ``Point``, ``beta:A,B`` a ``Beta``.

    This is the syntax of the command line and of rubric files.

    Args:
        text: The text, such as ``0.3`` or ``beta:8,2``.

    Returns:
        The credence.

    Raises:
        ValidationError: If the text is neither form, or its numbers are out of range. The message
            starts with the text, quoted.
    """
    kind, colon, rest = text.partition(":")
    numbers: list[float] | None = None
    if not colon:
        numbers = _floats([text])
    elif kind.strip().lower() == "beta":
        numbers = _floats(rest.split(","))
    try:
        if numbers is not None and len(numbers) == 1 and not colon:
            return Point(numbers[0])
        if numbers is not None and len(numbers) == 2 and colon:  # noqa: PLR2004 - the two shape parameters
            return Beta(*numbers)
    except ValidationError as error:
        msg = f"{text!r}: {error}"
        raise ValidationError(msg) from None
    msg = f"{text!r} is not a credence"
    raise ValidationError(msg)

JSON format

A graph serialises to a JSON document with "format": "credencegraph" and an integer "version" (currently 1). The structure and types are described by a published JSON Schema, available from credencegraph.core.json_schema() and shipped in the package as credencegraph/core/schema/graph-1.schema.json.

The schema covers structure only. Unique ids, relations that name existing nodes, the absence of cycles among requires, supports and refutes relations, and the ban on an equivalent or exclusive relation joining a node to itself are checked when a document is loaded, because a schema cannot express them.

A credence is a bare number p (a point value with 0 <= p <= 1) or an object {"alpha": a, "beta": b} with a, b > 0.