More on Formulas

In this chapter, you are going to learn more on Formula representations that are alternative to syntax trees. As you will see, for example, formulas with specific structure (e.g., normal forms) can be represented in ways that make them more easy to handle, and can lead to great benefits in terms of both computational and memory load.

We proceed by presenting the random formulae generation engine, parsing and some utility function.

Recalling the type hierarchy presented in man-core, it is here enriched with the following new types and structures.


Literals

Linear Forms

SoleLogics.LeftmostLinearFormType
struct LeftmostLinearForm{C<:Connective,SS<:SyntaxStructure} <: SyntaxStructure
    grandchildren::Vector{<:SS}
end

A syntax structure representing the foldl of a set of other syntax structure of type SS by means of a connective C. This structure enables a structured instantiation of formulas in conjuctive/disjunctive forms, and conjuctive normal form (CNF) or disjunctive normal form (DNF), defined as:

const LeftmostConjunctiveForm{SS<:SyntaxStructure} = LeftmostLinearForm{typeof(∧),SS}
const LeftmostDisjunctiveForm{SS<:SyntaxStructure} = LeftmostLinearForm{typeof(∨),SS}

const CNF{SS<:SyntaxStructure} = LeftmostLinearForm{typeof(∧),LeftmostLinearForm{typeof(∨),SS}}
const DNF{SS<:SyntaxStructure} = LeftmostLinearForm{typeof(∨),LeftmostLinearForm{typeof(∧),SS}}

Examples

julia> LeftmostLinearForm(→, parseformula.(["p", "q", "r"]))
LeftmostLinearForm{SoleLogics.NamedConnective{:→},Atom{String}}
    "(p) → (q) → (r)"

julia> LeftmostConjunctiveForm(parseformula.(["¬p", "q", "¬r"]))
LeftmostLinearForm{SoleLogics.NamedConnective{:∧},SyntaxTree}
    "(¬p) ∧ (q) ∧ (¬r)"

julia> LeftmostDisjunctiveForm{Literal}([Literal(false, Atom("p")), Literal(true, Atom("q")), Literal(false, Atom("r"))])
LeftmostLinearForm{SoleLogics.NamedConnective{:∨},Literal}
    "(¬p) ∨ (q) ∨ (¬r)"

julia> LeftmostDisjunctiveForm([LeftmostConjunctiveForm(parseformula.(["¬p", "q", "¬r"]))]) isa DNF
true

julia> conj = LeftmostConjunctiveForm(@atoms p q)
LeftmostConjunctiveForm with 2 Atom{String} grandchildren:
        p
        q

julia> tree(conj)
SyntaxBranch: p ∧ q

julia> nconj = NEGATION(conj)
LeftmostLinearForm with connective ¬ and 1 LeftmostConjunctiveForm{Atom{String}} grandchildren:
        (p) ∧ (q)

julia> tree(nconj)
SyntaxBranch: ¬(p ∧ q)

julia> tree(nconj ∧ nconj)
SyntaxBranch: ¬(p ∧ q) ∧ ¬(p ∧ q)

See also SyntaxStructure, SyntaxTree, LeftmostConjunctiveForm, LeftmostDisjunctiveForm, Literal.

source

<!–

SoleLogics.AnchoredFormulaType
struct AnchoredFormula{L<:AbstractLogic} <: Formula
    _logic::Base.RefValue{L}
    synstruct::SyntaxStructure
end

A formula anchored to a logic of type L, and wrapping a syntax structure. The structure encodes a formula belonging to the grammar of the logic, and the truth of the formula can be evaluated on interpretations of the same logic. Note that, here, the logic is represented by a reference.

Upon construction, the logic can be passed either directly, or via a RefValue. Additionally, the following keyword arguments may be specified:

  • check_atoms::Bool = false: whether to perform or not a check that the atoms belong to the alphabet of the logic;
  • check_tree::Bool = false: whether to perform or not a check that the formula's syntactic structure honors the grammar (includes the check performed with check_atoms = true);

Cool feature: a AnchoredFormula can be used for instating other formulas of the same logic. See the examples.

Examples

julia> φ = parseformula(AnchoredFormula, "◊(p→q)");

julia> f2 = φ(parseformula("p"));

julia> syntaxstring(φ)
"◊(→(p, q))"

julia> syntaxstring(f2)
"p"

julia> @assert logic(φ) == logic(f2)

julia> @assert ◊ in operators(logic(f2))

julia> @assert ◊ isa operatorstype(logic(f2))

See also AbstractLogic, logic, SyntaxToken, SyntaxBranch, tree.

source
SoleLogics.baseformulaMethod
function baseformula(
    φ::Formula;
    infer_logic = true,
    additional_operators::Union{Nothing,Vector{<:Operator}} = nothing,
    kwargs...
)

Attempt at instantiating a AnchoredFormula from a syntax token/formula, by inferring the logic it belongs to. If infer_logic is true, then a canonical logic (e.g., propositional logic with all the BASE_PROPOSITIONAL_CONNECTIVES) is inferred; if it's false, then a logic with exactly the operators appearing in the syntax tree, plus the additional_operators is instantiated.

Examples

julia> t = parseformula("◊((p∧q)→r)");

julia> unique(operators(logic(SoleLogics.baseformula(t))))
3-element Vector{Union{SoleLogics.NamedConnective{:→}, SoleLogics.NamedConnective{:◊}, SoleLogics.NamedConnective{:∧}}}:
 ∧
 ◊
 →

julia> unique(operators(logic(SoleLogics.baseformula(t; additional_operators = SoleLogics.BASE_MODAL_CONNECTIVES))))
8-element Vector{Union{SoleLogics.BottomOperator, SoleLogics.NamedConnective{:¬}, SoleLogics.NamedConnective{:∧}, SoleLogics.NamedConnective{:∨}, SoleLogics.NamedConnective{:→}, SoleLogics.NamedConnective{:◊}, SoleLogics.NamedConnective{:□}, SoleLogics.TopOperator}}:
 ¬
 ∧
 ∨
 →
 ◊
 □
source
SoleLogics.parseformulaFunction
parseformula(
    T::Type{AnchoredFormula},
    expr::AbstractString,
    additional_operators::Union{Nothing,Vector{<:Operator}} = nothing;
    operators::Union{Nothing,Vector{<:Operator}},
    grammar::Union{Nothing,AbstractGrammar} = nothing,
    algebra::Union{Nothing,AbstractAlgebra} = nothing,
    kwargs...
)::AnchoredFormula

Return a AnchoredFormula which is the result of parsing an expression via the Shunting yard algorithm. By default, this function is only able to parse operators in SoleLogics.BASE_PARSABLE_CONNECTIVES; additional operators may be provided as a second argument.

The grammar and algebra of the associated logic is inferred using the baseformula function from the operators encountered in the expression, and those in additional_operators.

See parseformula, baseformula, BASE_PARSABLE_CONNECTIVES.

source

–>

Random sampling and generation

SoleLogics.randatomFunction
randatom(
    [rng::Union{Random.AbstractRNG,Integer},]
    a::AbstractAlphabet,
    args...;
    kwargs...
)

Randomly generate an Atom from a finite AbstractAlphabet according to a uniform distribution.

Examples

julia> alphabet = ExplicitAlphabet(1:5)
ExplicitAlphabet{Int64}(Atom{Int64}[Atom{Int64}: 1, Atom{Int64}: 2, Atom{Int64}: 3, Atom{Int64}: 4, Atom{Int64}: 5])

julia> randatom(42, alphabet)
Atom{Int64}: 4

See also natoms, AbstractAlphabet.

source
randatom(
    [rng::Union{Random.AbstractRNG,Integer},]
    a::UnionAlphabet;
    atompicking_mode::Symbol=:uniform,
    subalphabets_weights::Union{Nothing,AbstractWeights,AbstractVector{<:Real}}=nothing
)::Atom

Sample an atom from a UnionAlphabet. By default, the sampling is uniform with respect to the atoms.

By setting atompicking_mode = :uniform_subalphabets one can force a uniform sampling with respect to the sub-alphabets.

Moreover, one can specify a :weighted atompicking_mode, together with a subalphabets_weights vector.

Examples

julia> alphabet1 = ExplicitAlphabet(Atom.(1:10));
julia> alphabet2 = ExplicitAlphabet(Atom.(11:20));
julia> union_alphabet = UnionAlphabet([alphabet1, alphabet2]);

julia> randatom(42, union_alphabet)
Atom{Int64}: 11

julia> randatom(42, union_alphabet; atompicking_mode=:uniform_subalphabets)
Atom{Int64}: 11

julia> for i in 1:10
            randatom(
                union_alphabet;
                atompicking_mode=:weighted,
                subalphabets_weights=[0.8,0.2]
            ) |> syntaxstring |> vcat |> print
        end
["6"]["3"]["10"]["7"]["2"]["2"]["6"]["9"]["20"]["16"]

See also UnionAlphabet.

source
StatsBase.sampleMethod
function StatsBase.sample(
    [rng::Union{Integer,AbstractRNG}=Random.GLOBAL_RNG,]
    alphabet::AbstractAlphabet,
    weights::AbstractWeights,
    args...;
    kwargs...
)

Sample an Atom from an alphabet, with probabilities proportional to the weights given in weights.

See also AbstractAlphabet, AbstractWeights, Atom.

source
SoleLogics.randformulaFunction
function randformula(
    [T::Type{<:Formula}=SyntaxTree,]
    [rng::Union{Integer,AbstractRNG}=Random.GLOBAL_RNG,]
    maxheight::Integer,
    alphabet::Union{AbstractVector,AbstractAlphabet},
    operators::AbstractVector{<:Operator},
    args...;
    maxmodaldepth::Integer=maxheight,
    atompicker::Union{Nothing,Function,AbstractWeights,AbstractVector{<:Real}}=randatom,
    opweights::Union{Nothing,AbstractWeights,AbstractVector{<:Real}}=nothing,
    alphabet_sample_kwargs::Union{Nothing,AbstractVector}=nothing,
    kwargs...
)

Return a pseudo-randomic formula of type T.

Arguments

  • rng::Union{Intger,AbstractRNG}=Random.GLOBAL_RNG: random number generator;
  • maxheight::Integer: maximum height of the generated structure;
  • alphabet::AbstractAlphabet: collection from which atoms are chosen randomly;
  • operators::AbstractVector{<:Operator}: vector from which legal operators are chosen.

Keyword Arguments

  • maxmodaldepth::Integer: maximum modal depth;
  • atompicker::Union{Nothing,Function,AbstractWeights,AbstractVector{<:Real}}: method used to pick a random element. For example, this could be Base.rand, StatsBase.sample or an array of integers or an array of StatsBase.AbstractWeights;
  • opweights::Union{Nothing,AbstractWeights,AbstractVector{<:Real}}: operators are sampled with probabilities proportional to this vector (see AbstractWeights of StatsBase package).
  • alphabet_sample_kwargs::AbstractVector: pool of atoms to pull from if the given alphabet is not finite.
  • basecase::Function = method to specify the base case of the recursion; if not specified, it returns atompicker.

[!WARNING] The basecase is applied at the end of the recustion (i.e., when height = 0). If introducting a basecase which produces a subformula, please adjust the maxheight parameter value accordingly (e.g., when producing a subformula of the type o(p,q), where o is a connective and p,q are atoms, to generate a formula of maxheight n provide a value of n-1 for the maxheight parameter).

  • mode::Bool = :maxheight constrains the generated syntax tree to having a height smaller or equal to maxheight (mode = :maxheight), to having height equal to maxheight (mode = :exactheight), or to having height equal to maxheight and being full (mode = :full),.
  • earlystoppingtreshold::Float : when mode = :maxheight, controls the probability of calling the basecase before reaching.

Examples

julia> syntaxstring(randformula(4, ExplicitAlphabet([1,2]), [NEGATION, CONJUNCTION, IMPLICATION]))
"¬((¬(¬(2))) → ((1 → 2) → (1 → 2)))"

See also AbstractAlphabet, AbstractWeights, Atom, Operator, SyntaxBranch, SyntaxTree.

source
function randformula(
    [T::Type{<:Formula}=SyntaxTree,]
    [rng::Union{Integer,AbstractRNG}=Random.GLOBAL_RNG,]
    maxheight::Integer,
    [g::AbstractGrammar,]
    args...;
    kwargs...
)

Fallback to randformula, specifying only the maxheight (possibly also a grammar) of the generated SyntaxTree.

See also AbstractGrammar, randformula(::Integer, ::Union{AbstractVector,AbstractAlphabet}, ::AbstractVector).

source

Parsing

SoleLogics.BASE_PARSABLE_CONNECTIVESConstant
const BASE_PARSABLE_CONNECTIVES = SoleLogics.Syntactical[¬, ∧, ∨, →, ◊, □, ⟨G⟩, [G], ⟨=⟩, [=], ⊤, ⊥]

Vector of (standard) operators that are automatically taken care of when parsing. These are ¬, ∧, ∨, →, ◊, □, ⟨G⟩, [G], ⟨=⟩, [=], ⊤ and ⊥.

See also parseformula.

source
SoleLogics.parseformulaFunction
parseformula(
    [F::Type{<:Formula}=SyntaxTree],
    expr::AbstractString,
    additional_operators::Union{Nothing,AbstractVector} = nothing;
    kwargs...
)::F

parseformula(
    F::Type{<:SyntaxTree},
    expr::AbstractString,
    logic::AbstractLogic;
    kwargs...
)::F

Parse a formula of type F from a string expression (its syntaxstring).

By default, this function is only able to parse operators in SoleLogics.BASE_PARSABLE_CONNECTIVES (e.g., ¬, ∧, ∨ and →); additional, non-standard operators may be provided as a vector additional_operators, and their syntaxstrings will be used for parsing them. Note that, in case of clashing syntaxstrings, the provided additional operators will override the standard ones.

When parsing SyntaxTrees, the Shunting yard algorithm is used, and the method allows the following keywords arguments.

Keyword Arguments

  • function_notation::Bool = false: if set to true, the expression is considered in function notation (e.g., "⨁(arg1, arg2)"); otherwise, it is considered in infix notation (e.g., "arg1 ⨁ arg2");
  • atom_parser::Base.Callable = Atom{String}: a callable to be used for parsing atoms, once they are recognized in the expression. It must return the atom, or the Atom itself;
  • additional_whitespaces::Vector{Char} = Char[]: characters to be stripped out from each syntax token. For example, if '@' in additional_whitespaces, "¬@p@" is parsed just as "¬p".
  • opening_parenthesis::String = "(": the string signaling the opening of an expression block;
  • closing_parenthesis::String = ")": the string signaling the closing of an expression block;
  • arg_delim::String = ",": when function_notation = true, the string that delimits the different arguments of a function call.
Warning

For a proper functioning, the syntaxstring of any syntax token cannot be prefixed/suffixed by whitespaces. For example, for any operator , it should hold that syntaxstring(⨁) == strip(syntaxstring(⨁)). Also, syntaxstrings cannot contain special symbols (opening_parenthesis, closing_parenthesis, and arg_delim) as substrings.

Examples

julia> syntaxstring(parseformula("¬p∧q∧(¬s∧¬z)"))
"¬p ∧ q ∧ ¬s ∧ ¬z"

julia> syntaxstring(parseformula("∧(¬p,∧(q,∧(¬s,¬z)))", function_notation=true))
"¬p ∧ q ∧ ¬s ∧ ¬z"

julia> syntaxstring(parseformula("¬1→0"; atom_parser = (x -> Atom{Float64}(parse(Float64, x)))))
"(¬1.0) → 0.0"
Note

For any Formula type F, this function should be the inverse of syntaxstring; that is, if φ::F then the following should hold, for at least some args, and for every kwargs allowing correct parsing: φ == parseformula(F, syntaxstring(φ, args...; kwargs...), args...; kwargs...).

See also SyntaxTree, BASE_PARSABLE_CONNECTIVES, syntaxstring.

source

Utilities

SoleLogics.treewalkMethod
treewalk(
    st::SyntaxTree,
    args...;
    rng::AbstractRNG = Random.GLOBAL_RNG,
    criterion::Function = ntokens,
    toleaf::Bool = true,
    returnnode::Bool = false,
    transformnode::Function = nothing
)::SyntaxTree

Return a subtree of syntax tree, by following these options:

  • criterion: function used to compute the probability of stopping at a random node;
  • returnnode: true if only the subtree is to be returned;
  • transformnode: function that will be applied to the chosen subtree.
source
SoleLogics.subformulasMethod
subformulas(f::Formula; sorted=true)

Return all sub-formulas (sorted by size when sorted=true) of a given formula.

Examples

julia> syntaxstring.(SoleLogics.subformulas(parseformula("◊((p∧q)→r)")))
6-element Vector{String}:
 "p"
 "q"
 "r"
 "p ∧ q"
 "◊(p ∧ q)"
 "(◊(p ∧ q)) → r"

See also SyntaxTree), Formula.

source
SoleLogics.normalizeMethod
normalize(
    f::Formula;
    profile = :readability,
    remove_boxes = nothing,
    reduce_negations = true,
    simplify_constants = true,
    allow_atom_flipping = false,
    prefer_implications = false,
    remove_implications = false,
    forced_negation_removal = nothing,
    remove_identities = true,
    unify_toones = true,
    rotate_commutatives = true,
    flip_atom = nothing,
)

Return a modified version of a given formula, that has the same semantics but different syntax. This is useful for simplifying formulas for readability, or when checking the truth of many (possibly semantically similar) formulas; for example, when performing model checking. The current implementation assumes the underlying algebra is Boolean!

Arguments

  • f::Formula: when set to true, the formula;
  • profile::Symbol: possible values are :readability, which optimizes for qualitative simplicity for a human to understand, and :modelchecking, which optimizes model checking speed;
  • remove_boxes::Bool: remove all (non-relational and relational) box operators by using the equivalence ◊φ ≡ ¬□¬φ. Note: this assumes an underlying Boolean algebra.
  • reduce_negations::Bool: when set to true, attempts at reducing the number of negations by appling some transformation rules (e.g., De Morgan's laws). Note: this assumes an underlying Boolean algebra.
  • allow_atom_flipping::Bool: when set to true, together with reduce_negations=true, this may cause the negation of an atom to be replaced with the its dual atom.
  • flip_atom::Union{Nothing,Callable}: when a callable is provided, it is used for deciding when to flip atoms.

Examples

julia> f = parseformula("□¬((p∧¬q)→r)∧⊤");

julia> syntaxstring(f)
"□¬((p ∧ ¬q) → r) ∧ ⊤"

julia> syntaxstring(SoleLogics.normalize(f; profile = :modelchecking, allow_atom_flipping = false))
"¬◊(q ∨ ¬p ∨ r)"

julia> syntaxstring(SoleLogics.normalize(f; profile = :readability, allow_atom_flipping = false))
"□(¬r ∧ p ∧ ¬q)"

See also SyntaxTree), Formula.

source
SoleLogics.isgroundedMethod
isgrounded(f::Formula)::Bool

Return true if the formula is grounded, that is, if it can be inferred from its syntactic structure that, given any frame-based model, the truth value of the formula is the same on every world.

Examples

julia> f = parseformula("⟨G⟩p → [G]q");

julia> syntaxstring(f)
"(⟨G⟩p) → ([G]q)"

julia> SoleLogics.isgrounded(f)
true

See also isgrounding), SyntaxTree), Formula.

source