Introduction
At the end of this chapter, you are going to understand what modal logic is, and why it is so important from a computational standpoint of view, with respect to propositional logic and first order logic. For those of you who want to fully immerse in the topic, we recommend reading the thesis Foundations of Modal Symbolic Learning, by Stan Ionel Eduard.
Recalling the type hierarchy presented in man-core, it is here enriched with the following new types and structures.
Pills of Modal Logic
Narrowly speaking, modal logic was initially investigated as the logic of necessary and possible truths judgments due to Aristotle's foresighted analysis of statements containing the words "necessary" and "possible".
Modal logic is, essentially, Propositional Logic enriched with a set of modal connectives (or modalities), that are used to express some form of quantification over a set of entities. Interpretations in modal logic act as directed, possibly many-relation graphs of propositional assigments, and are called Kripke structures. The vertices in the graph model the entities, and are called possible worlds (or, simply, worlds). Furthermore, worlds are connected via one or more accessibility relations, and each world has a propositional assignment. Modal formula are, generally, interpreted on specific worlds of Kripke structures.
Why Modal Logic? An introduction
Very often real-world applications give rise to non-static sets of data (e.g., temporal, spatial, graph-based data). The standard approach is to pre-process such data, so that their aspect becomes easy to fit in a tabular structure (e.g., datasets, spreadsheets).
Pre-processing non-static data means denaturing it: we want to deal with this kind of data natively, avoiding losing valuable information about its structure. Because of this, we require a logic which is more expressive than propositional one. This is where modal logic comes in.
Imagine the following scenario: you have a speech audio sample of $9$ seconds and you want to establish whether the propositional letter $p$ is true or no on the sample. Let's say that $p$ is the fact the audio is loud. The latter is referred to the sample in its entirety: by looking at it as a whole we can compute whether $p$ is true or no. Let's say that $p$ is true. We can easily schematize the situation by just drawing a graph with a single node containing $p$, where the node is the entire audio.

This single node above is what we call world, and it is exactly a propositional model. We can represent it as follows.
p = Atom("p") # "the audio is loud"
world = SoleLogics.World(1) # this is just an abstract reference to a 9s audio sample
# ... calculations here ...
valuation = Dict([world => TruthDict(p => true)]) # after some calculations, we establish "p" is true in "world"Now here's a challenge. Try to express the following fact using just propositional logic: the audio contains at least two pauses of three seconds that are interspersed with each other by three or more seconds of loud speaking. After a few tries you should be convinced that this is not possible, because through propositional logic we are not able to quantify over relations inside the sample. What we can do instead, is upgrade propositional logic to modal logic. We have to deal in a more granular manner with the original audio sample, and we don't want to denature it. Here is what we need:
- an
Atom$p$, representing the fact the audio is loud; - an
Atom$q$, representing the fact the audio is silence; Worlds describing small pieces of the original audio. In particular, we establish that each world is representative for $3$ seconds of audio;- an accessibility relation (a specific
AbstractRelation) to express which worlds are reachable from another through a modalNamedConnective.
The situation is simply schematized as follows

where each world identifies $3$ seconds of audio, and we consider a relation between two worlds only if they represent adjacent parts of the audio. At this point, let's create the Kripke structure in the example using SoleLogics.jl.
p = Atom("p") # "the audio is loud"
q = Atom("q") # "the audio is silence"
# Worlds representing 3-second-pieces of the original audio
worlds = [SoleLogics.World(1), SoleLogics.World(2), SoleLogics.World(3)]
edges = Edge.([(1,2), (1,3), (2,3)])
kripkeframe = SoleLogics.ExplicitCrispUniModalFrame(worlds, Graphs.SimpleDiGraph(edges))
valuation = Dict([worlds[1] => TruthDict([p => false, q => true]), worlds[2] => TruthDict([p => true, q => false]), worlds[3] => TruthDict([p => false, q => true])])
kripkestructure = KripkeStructure(kripkeframe, valuation)In the Kripke structure above, the central world $w_2$ (the only one where $p$ is true) is accessible from the leftmost world $w_1$ (where $q$ is true). In modal logic terms, this is expressed by the notation
\[K,w_1 \models \lozenge p\]
where $K$ is a Kripke structure, $w_1$ is the leftmost world in the image, and $\lozenge p$ means look at the world accessibles from $w_1$ and check whether p is true or false on those neighbors.
Now we are ready to resume the long statement of a few paragraphs ago, the one we could not express using only propositional logic. We can translate it using modal logic! The formula we are looking for is
\[q \wedge \lozenge p \wedge \lozenge \lozenge q\]
which has to be read check whether this sub-sample of audio is silence, and the sub-sample after this is loud, and the sub-sample after the latter is silence again. Let's see if this formula is true on the Kripke structure above, starting from $w_1$.
# ... continuing code above ...
phi = ∧(q, ∧(◊(p),◊(◊(q)))) # \wedge+TAB can also be written as CONJUNCTION, while \lozenge+TAB is called DIAMOND and is a modal operator
check(phi, kripkestructure, worlds[1]) # prints trueBy reading the following sections, you will better grasp how Worlds are defined, as well as relations (AbstractRelation), how those two concepts are bound togheter in AbstractFrames and KripkeStructures. You will also understand how to access one world from another by using (or implementing) Connectives such as ◊ (or DIAMOND) and the accessibles method.
Worlds and Frames
SoleLogics.AbstractWorld — Typeabstract type AbstractWorld endAbstract type for the nodes of an annotated accessibility graph (Kripke structure). This is used, for example, in modal logic, where the truth of formulas is relativized to worlds, that is, nodes of a graph.
Implementing
When implementing a new world type, the logical semantics should be defined via accessibles methods; refer to the help for accessibles.
See also AbstractKripkeStructure, AbstractFrame.
SoleLogics.World — Typestruct World{T} <: AbstractWorld
name::T
endA world that is solely identified by its name. This can be useful when instantiating the underlying graph of a modal frame in an explicit way.
See also OneWorld, AbstractWorld.
SoleLogics.AbstractFrame — Typeabstract type AbstractFrame{W<:AbstractWorld} endAbstract type for an accessibility graph (Kripke frame), that gives the topology to Kripke structures. A frame can be queried for its set of vertices (also called worlds, see allworlds), and it can be browsed via its accessibility relation(s) (see accessibles). Refer to FullDimensionalFrame as an example.
See also truthtype, , allworlds, nworlds, AbstractKripkeStructure, AbstractWorld.
SoleLogics.worldtype — Methodworldtype(fr::AbstractFrame)
worldtype(i::AbstractKripkeStructure)Return the world type of the Kripke frame/structure.
See also AbstractFrame.
SoleLogics.allworlds — Methodallworlds(fr::AbstractFrame{W})::AbstractVector{<:W} where {W<:AbstractWorld}Return all worlds within the frame.
See also nworlds, AbstractFrame.
SoleLogics.nworlds — Methodnworlds(fr::AbstractFrame)::IntegerReturn the number of worlds within the frame.
See also nworlds, AbstractFrame.
SoleLogics.AbstractUniModalFrame — Typeabstract type AbstractUniModalFrame{W<:AbstractWorld} <: AbstractFrame{W} endA frame of a modal logic based on a single (implicit) accessibility relation.
See also AbstractMultiModalFrame, AbstractFrame.
SoleLogics.accessibles — Methodaccessibles(fr::AbstractUniModalFrame{W}, w::W)::Worlds{W} where {W<:AbstractWorld}Return the worlds in frame fr that are accessible from world w.
See also AbstractWorld, AbstractUniModalFrame.
Relations
SoleLogics.AbstractRelation — Typeabstract type AbstractRelation endAbstract type for the relations of a multi-modal annotated accessibility graph (Kripke structure). Two noteworthy relations are identityrel and globalrel, which access the current world and all worlds, respectively.
Examples
julia> fr = SoleLogics.FullDimensionalFrame((10,), Interval{Int});
julia> Interval(8,11) in (accessibles(fr, Interval(2,5), IA_L))
trueImplementation
When implementing a new relation type R, please provide the methods:
arity(::R)::Int = ...
syntaxstring(::R; kwargs...)::String = ...If the relation is symmetric, please specify its converse relation cr with:
hasconverse(::R) = true
converse(::R) = crIf the relation is many-to-one or one-to-one, please flag it with:
istoone(::R) = trueIf the relation is reflexive or transitive, flag it with:
isreflexive(::R) = true
istransitive(::R) = trueMost importantly, the logical semantics for R should be defined via accessibles methods; refer to the help for accessibles.
See also issymmetric, isreflexive, istransitive, isgrounding, arity, syntaxstring, converse, hasconverse, istoone, identityrel, globalrel, accessibles, AbstractKripkeStructure, AbstractFrame, AbstractWorld.
SoleLogics.arity — MethodSoleLogics.converse — Methodhasconverse(r::AbstractRelation)::Bool
converse(r::AbstractRelation)::AbstractRelationIf the relation hasconverse, return the converse relation (type) of a given relation (type).
See also issymmetric, isreflexive, istransitive, AbstractRelation.
SoleLogics.istoone — Methodistoone(r::AbstractRelation) = falseReturn whether it is known that a relation is istoone.
See also hasconverse, converse, issymmetric, istransitive, isgrounding, AbstractRelation.
SoleLogics.issymmetric — Methodissymmetric(r::AbstractRelation) = hasconverse(r) ? converse(r) == r : falseReturn whether it is known that a relation is symmetric.
See also hasconverse, converse, isreflexive, istransitive, isgrounding, AbstractRelation.
SoleLogics.isreflexive — Methodisreflexive(::AbstractRelation)Return whether it is known that a relation is reflexive.
See also issymmetric, istransitive, isgrounding, AbstractRelation.
SoleLogics.istransitive — Methodistransitive(::AbstractRelation)Return whether it is known that a relation is transitive.
See also istoone, issymmetric, isgrounding, AbstractRelation.
SoleLogics.isgrounding — Methodisgrounding(::AbstractRelation)Return whether it is known that a relation is grounding. A relation R is grounding if ∀x,z,y R(x,y) ⇔ R(z,y).
See also isreflexive, issymmetric, istransitive, AbstractRelation.
More on Frames and Kripke Structures
SoleLogics.AbstractMultiModalFrame — Typeabstract type AbstractMultiModalFrame{W<:AbstractWorld} <: AbstractFrame{W} endA frame of a multi-modal logic, that is, a modal logic based on a set of accessibility relations.
Implementation
When implementing a new multi-modal frame type, the logical semantics for the frame should be defined via accessibles methods; refer to the help for accessibles.
See also AbstractUniModalFrame, AbstractFrame.
SoleLogics.accessibles — Methodaccessibles(
fr::AbstractMultiModalFrame{W},
w::W,
r::AbstractRelation
) where {W<:AbstractWorld}Return the worlds in frame fr that are accessible from world w via relation r.
Examples
julia> fr = SoleLogics.FullDimensionalFrame((10,), Interval{Int});
julia> typeof(accessibles(fr, Interval(2,5), IA_L))
Base.Generator{...}
julia> typeof(accessibles(fr, globalrel))
Base.Generator{...}
julia> @assert SoleLogics.nworlds(fr) == length(collect(accessibles(fr, globalrel)))
julia> typeof(accessibles(fr, Interval(2,5), identityrel))
Vector{Interval{Int64}}
julia> Interval(8,11) in collect(accessibles(fr, Interval(2,5), IA_L))
trueImplementation
Since accessibles always returns an iterator to worlds of the same type W, the current implementation of accessibles for multi-modal frames delegates the enumeration to a lower level _accessibles function, which returns an iterator to parameter tuples that are, then, fed to the world constructor the using IterTools generators, as in:
function accessibles(
fr::AbstractMultiModalFrame{W},
w::W,
r::AbstractRelation,
) where {W<:AbstractWorld}
IterTools.imap(W, _accessibles(fr, w, r))
endAs such, when defining new frames, worlds, and/or relations, one should provide new methods for _accessibles. For example:
_accessibles(fr::Full1DFrame, w::Interval{<:Integer}, ::_IA_A) = zip(Iterators.repeated(w.y), w.y+1:X(fr)+1)This pattern is generally convenient; it can, however, be bypassed, although this requires defining two additional methods in order to resolve dispatch ambiguities. When defining a new frame type FR{W}, one can resolve the ambiguities and define a custom accessibles method by providing these three methods:
# access worlds through relation `r`
function accessibles(
fr::FR{W},
w::W,
r::AbstractRelation,
) where {W<:AbstractWorld}
...
end
# access current world
function accessibles(
fr::FR{W},
w::W,
r::IdentityRel,
) where {W<:AbstractWorld}
[w]
end
# access all worlds
function accessibles(
fr::FR{W},
w::W,
r::GlobalRel,
) where {W<:AbstractWorld}
allworlds(fr)
endIn general, it should be true that collect(accessibles(fr, w, r)) isa AbstractWorlds{W}.
See also AbstractWorld, AbstractRelation, AbstractMultiModalFrame.
SoleLogics.AbstractKripkeStructure — Typeabstract type AbstractKripkeStructure <: AbstractInterpretation endAbstract type for representing Kripke structures's. It comprehends a directed graph structure (Kripke frame), where nodes are referred to as worlds, and the binary relation between them is referred to as the accessibility relation. Additionally, each world is associated with a mapping from Atoms to Truth values.
See also frame, worldtype, accessibles, AbstractInterpretation.
SoleLogics.check — Methodcheck(
φ::Formula,
i::AbstractInterpretation,
args...;
kwargs...
)::BoolCheck a formula on a logical interpretation (or model), returning true if the truth value for the formula istop. This process is referred to as (finite) model checking, and there are many algorithms for it, typically depending on the complexity of the logic.
Examples
julia> @atoms String p q
2-element Vector{Atom{String}}:
Atom{String}("p")
Atom{String}("q")
julia> td = TruthDict([p => TOP, q => BOT])
TruthDict with values:
┌────────┬────────┐
│ q │ p │
│ String │ String │
├────────┼────────┤
│ ⊥ │ ⊤ │
└────────┴────────┘
julia> check(CONJUNCTION(p,q), td)
falseSee also interpret, Formula, AbstractInterpretation, TruthDict.
SoleLogics.KripkeStructure — Typestruct KripkeStructure{
FR<:AbstractFrame,
MAS<:AbstractDict
} <: AbstractKripkeStructure
frame::FR
assignment::AS
endType for representing Kripke structures. explicitly; it wraps a frame, and an abstract dictionary that assigns an interpretation to each world.
Modal Connectives
SoleLogics.ismodal — Methodismodal(::Type{<:Connective})::Bool = false
ismodal(c::Connective)::Bool = ismodal(typeof(c))Return whether it is known that an Connective is modal.
Examples
julia> ismodal(◊)
true
julia> ismodal(∧)
falseSoleLogics.isbox — Methodisbox(::Type{<:Connective})::Bool = false
isbox(c::Connective)::Bool = isbox(typeof(c))Return whether it is known that an Connective is a box (i.e., universal) connective.
Examples
julia> SoleLogics.isbox(◊)
false
julia> SoleLogics.isbox(∧)
false
julia> SoleLogics.isbox(□)
trueSoleLogics.DIAMOND — Constantconst DIAMOND = NamedConnective{:◊}()
const ◊ = DIAMOND
ismodal(::typeof(◊)) = true
arity(::typeof(◊)) = 1Logical diamond connective, typically interpreted as the modal existential quantifier. See here.
See also BOX, NamedConnective, Connective.
SoleLogics.BOX — Constantconst BOX = NamedConnective{:□}()
const □ = BOX
arity(::typeof(□)) = 1Logical box connective, typically interpreted as the modal universal quantifier. See here.
See also DIAMOND, NamedConnective, Connective.
Relational Connectives
SoleLogics.AbstractRelationalConnective — Typeabstract type AbstractRelationalConnective{R<:AbstractRelation} <: Connective endAbstract type for relational logical connectives. A relational connective allows for semantic quantification across relational structures (e.g., Kripke structures). It has arity equal to the arity of its underlying relation minus one.
See, for example temporal modal logic.
See also DiamondRelationalConnective, BoxRelationalConnective, AbstractKripkeStructure, AbstractFrame.
SoleLogics.relationtype — Methodrelationtype(::AbstractRelationalConnective{R}) where {R<:AbstractRelation} = R
relation(op::AbstractRelationalConnective) = relationtype(op)()Return the underlying relation (and relation type) of the relational connective.
See also AbstractFrame.
SoleLogics.DiamondRelationalConnective — Typestruct DiamondRelationalConnective{R<:AbstractRelation} <: AbstractRelationalConnective{R} end
struct BoxRelationalConnective{R<:AbstractRelation} <: AbstractRelationalConnective{R} endSingleton types for relational connectives, typically interpreted as the modal existential and universal quantifier, respectively.
Both connectives can be easily instantiated with relation instances, such as DiamondRelationalConnective(rel), which is a shortcut for DiamondRelationalConnective{typeof(rel)}().
Examples
julia> syntaxstring(DiamondRelationalConnective(IA_A))
"⟨A⟩"
julia> syntaxstring(BoxRelationalConnective(IA_A))
"[A]"
julia> @assert DiamondRelationalConnective(IA_A) == SoleLogics.dual(BoxRelationalConnective(IA_A))
See also DiamondRelationalConnective, BoxRelationalConnective, syntaxstring, dual, AbstractKripkeStructure, AbstractFrame.
SoleLogics.diamond — Methoddiamond() = DIAMOND
diamond(r::AbstractRelation) = DiamondRelationalConnective(r)Return either the diamond modal connective from unimodal logic (i.e., ◊), or a a diamond relational connective from a multi-modal logic, wrapping the relation r.
See also DiamondRelationalConnective, diamond, DIAMOND.
SoleLogics.box — Methodbox() = BOX
box(r::AbstractRelation) = BoxRelationalConnective(r)Return either the box modal connective from unimodal logic (i.e., □), or a a box relational connective from a multi-modal logic, wrapping the relation r.
See also BoxRelationalConnective, box, BOX.
Final steps
SoleLogics.modallogic — Methodmodallogic(;
alphabet = AlphabetOfAny{String}(),
operators = [⊤, ⊥, ¬, ∧, ∨, →, ◊, □],
grammar = CompleteFlatGrammar(AlphabetOfAny{String}(), [⊤, ⊥, ¬, ∧, ∨, →, ◊, □]),
algebra = BooleanAlgebra(),
)Instantiate a modal logic given a grammar and an algebra. Alternatively, an alphabet and a set of operators can be specified instead of the grammar.
Examples
julia> (¬) isa operatorstype(modallogic());
true
julia> (□) isa operatorstype(modallogic());
true
julia> (□) isa operatorstype(modallogic(; operators = [¬, ∨]))
┌ Warning: Instantiating modal logic (via `modallogic`) with solely propositional operators (SoleLogics.NamedConnective[¬, ∨]). Consider using propositionallogic instead.
└ @ SoleLogics ~/.julia/dev/SoleLogics/src/modal-logic.jl:642
false
julia> modallogic(; alphabet = ["p", "q"]);
julia> modallogic(; alphabet = ExplicitAlphabet([Atom("p"), Atom("q")]));
See also propositionallogic, AbstractAlphabet, AbstractAlgebra.
SoleLogics.collateworlds — Methodcollateworlds(
fr::AbstractFrame{W},
op::Operator,
t::NTuple{N,WS},
)::AbstractVector{<:W} where {N,W<:AbstractWorld,WS<:AbstractWorlds}For a given crisp frame (truthtype == Bool), return the set of worlds where a composed formula op(φ1, ..., φN) is true, given the N sets of worlds where the each immediate sub-formula is true.
See also check, iscrisp, Operator, AbstractFrame.