NAME

Logic::Probabilistic - Probabilistic Logic Programming in Perl.

VERSION

Version 0.01

SYNOPSIS

use v5.38;
use Logic::Probabilistic::Syntax;

prob_logic AlarmNet {
    pfacts {
        0.7 => burglary;
        0.2 => earthquake;
    }

    prule 0.9 => alarm { burglary; earthquake; }
    prule 0.8 => alarm { burglary; not { earthquake; } }
    prule 0.1 => alarm { not { burglary; } earthquake; }
}

# Condition on observing alarm
query_prob AlarmNet::burglary, evidence => { alarm => 1 } -> my $b_given_alarm;
say "P(burglary | alarm):   " . $b_given_alarm->probability;   # ~0.9897

query_prob AlarmNet::earthquake, evidence => { alarm => 1 } -> my $e_given_alarm;
say "P(earthquake | alarm): " . $e_given_alarm->probability; # ~0.2276

DESCRIPTION

Logic::Probabilistic builds on Logic::Relational to allow the association of probabilities with facts and rules, and queries that return probabilities and probability distributions. To do this, it uses Sato's Distribution Semantics to define and query over possible worlds.

Logic::Probabilistic is most straightforwardly used with its syntax layer, Logic::Probabilistic::Syntax, which adds extra syntax to that of Logic::Relational for declaring probabilistic logic programs, probabilistic facts and rules.

USAGE

Declaring a Probabilistic Model

use prob_logic modelName { ... } to create a probabilistic model package.

prob_logic CoinGame {
    # Independent Bernoulli probabilistic fact: 70% chance of heads
    pfact 0.70 => heads;

    # Deterministic rule: winning requires heads
    rule win {
        heads;
    }
}

Declaring Probabilistic Facts (pfact)

use pfact and give the probability (between 0 and 1 inclusive).

pfact 0.7 => burglary;
pfact 0.2 => earthquake;

Probabilistic facts can take arguments. Each ground instance represents an independent random variable.

pfact 0.9 => reliable_link('server_a', 'router_1');
pfact 0.1 => unreliable_link('router_1', 'server_b');

When declaring multiple probabilistic facts, you can group them into a single pfacts { ... } block without repeating the pfact keyword on every line.

pfacts {
    # Propositional events
    0.01 => burglary;
    0.02 => earthquake;

    # Parameterized relations and fractions
    0.90 => link('server_a', 'router_1');
    0.85 => link('router_1', 'server_b');
    1/6  => die_face(1);
}

fact and pfact

Use fact rather than pfact 1.0 when you want to declare definitive background knowledge.

fact throws('John');
fact taxes;
fact edge('a', 'b');

It is important to know when to use fact rather than pfact. Although it is supeficially equivalent to pfact 1.0 , a fact is stored directly in the underlying relational logic engine and is not treated as a probabilistic variable, therefore it does not introduce any extra computational demand or create additional possible worlds. However, its associated probability cannot be changed from 1 using update_pfact at a later time, making it unsuitable for learning or adaptive models.

Categorical Choices and Annotated Disjunctions (pchoice)

Sometimes a variable has multiple mutually exclusive outcomes rather than a simple true/false state (e.g. rolling a 6-sided die). These variables can be declared using pchoice.

pchoice prize => [
    1/3 => 'door1',
    1/3 => 'door2',
    1/3 => 'door3',
];

In any possible world, exactly one outcome is active. You can also specify an optional null probability (where no outcome is chosen) if the probabilities do not sum to 1.0.

Rules and Probabilistic Rules

Rules (rule) are Horn clauses as per Logic::Relational.

# Head with no arguments:
rule alarm {
    burglary;
    alarm_cause_burglary;
}

# Head with arguments and logical variables:
rule path($x, $y) {
    edge($x, $y);
}

rule path($x, $z) {
    edge($x, $y);
    path($y, $z);
}

Use prule to associate a probability with a rule.

prule 0.5 => heads($c) {
    coin($c);
}

prule 0.1 => calls($x) {
    not { alarm; }
    person($x);
}

prule 0.2 => influences($x, $y) {
    person($x);
    person($y);
    $x \= $y;
}

# Propositional (0-arity) probabilistic rule
prule 0.9 => alarm {
    burglary;
    earthquake;
}

A prule is formally de-sugared into two components: An independent auxiliary pfact with the given probability (the 'cause'), and a deterministic Horn clause (rule) which combines the body with the auxiliary cause. To have explicit control over the underlying variables, use this pattern directly, e.g.

prob_logic BrokenWindow {
    # 1. Background facts
    fact throws('john');            # John throws with certainty (P = 1.0)
    pfact 0.50 => throws('mary');   # Mary throws with 50% probability

    # 2. Auxiliary causal mechanisms (probability that a throw breaks the glass)
    pfact 0.80 => break_cause('mary');
    pfact 0.60 => break_cause('john');

    # 3. Deterministic relational rule combining preconditions with causes
    rule break {
        fresh my $person;
        throws($person);
        break_cause($person);
    }
}

Logical Inversion and Negation

You can query or condition on the negation of predicates using not { ... }.

rule not_cloudy {
    not {
        rain;
    };
}

For query-level negative evidence, you can also use not_goal(...) or boolean hash values (see below sections on querying).

Querying Point Probabilities with query_prob

query_prob evaluates a query against a model, optionally conditioning on observed evidence.

query_prob MyModel::alarm -> my $result;
say "P(alarm) = " . $result->probability;

query_prob returns a Logic::Probabilistic::Result with the following methods and overloading:

Overloading

Numeric context causes the result to evaluate directly to the float probability.

if ( $result > 0.50 ) { ... }

String context formats the result into a summary including the probability and containing statistics relating to the inference engine (see below).

Methods

  • $result->probability: Point probability estimate (float in [0.0, 1.0]).

  • $result->ci($level): Lower and upper bounds of the Wilson score interval (default level 0.95).

  • $result->ci_low($level) / $result->ci_high($level): Individual interval endpoints.

  • $result->method: The inference method used (exact, rejection, or mcmc).

  • $result->total_samples: Number of worlds or samples evaluated.

  • $result->accepted_samples: Number of samples satisfying the evidence.

  • $result->r_hat: Gelman-Rubin convergence metric (MCMC only).

  • $result->ess: Effective Sample Size accounting for autocorrelation (MCMC only).

  • $result->chains: Number of Markov chains executed (MCMC only).

  • $result->is_converged($threshold): Boolean checking whether r_hat <= 1.05.

  • $result->warnings: Array of diagnostic warning messages.

  • $result->has_gradients: Boolean checking whether analytical gradients are available.

  • $result->gradients: Hashref of exact parameter partial derivatives ({ $param => $derivative }).

  • $result->gradient($target): Partial derivative with respect to a specific parameter name or pfact object.

Providing Evidence

Evidence can be specified using either evidence or given, supporting both positive observations and negative observations. If the observed evidence has zero probability, the engine raises the error: "Zero probability evidence: evidence cannot be satisfied in any possible world".

Boolean Hash Syntax

# Condition on alarm being true and earthquake being false
query_prob MyModel::burglary,
    evidence => { alarm => 1, earthquake => 0 } -> my $result;   

List of Goals

# Condition on both John and Mary calling
query_prob MyModel::burglary,
    evidence => [ MyModel::calls('John'), MyModel::calls('Mary') ] -> 
    my $result;

Pair List Syntax

# Flat list of pairs
query_prob MyModel::burglary,
    evidence => [ MyModel::calls('John') => 1, MyModel::calls('Mary') => 1 ] -> 
    my $result;

# Nested list of pairs
query_prob MyModel::burglary,
    evidence => [ [ MyModel::calls('John') => 1, MyModel::calls('Mary') => 1 ] ] -> 
    my $result;

# Array of separate pairs
query_prob MyModel::burglary,
    evidence => [   [ MyModel::calls('John') => 1 ], 
                    [ MyModel::calls('Mary') => 0 ] ] -> 
    my $result;

Direct Negation (not_goal)

query_prob MyModel::win,
    evidence => [ not_goal(MyModel::twoHeads()) ] -> my $result;

Querying Probability Distributions with query_dist

When a query contains an unbound logical variable, you can query the complete probability distribution over all possible unified values using query_dist.

fresh my $who;
query_dist BrokenWindow::throws($who) -> my $dist;

say "Probability distribution over throwers:";
say $dist;

Here, $dist is a Logic::Probabilistic::Distribution object providing moments (mean, variance, standard deviation), quantiles, CDFs, entropy, sampling, and formatted ASCII histogram tables.

Overloading

Printing $dist directly outputs a formatted statistical summary and ASCII bar chart.

Methods

  • $dist->prob($val): Probability of a specific value (0.0 if not observed).

  • $dist->mean, $dist->variance, $dist->std_dev: Statistical moments for numeric distributions.

  • $dist->mode: Most probable value.

  • $dist->min, $dist->max: Extrema of numeric values.

  • $dist->entropy($base): Shannon entropy (default base 2, in bits).

  • $dist->cdf($val): Cumulative distribution function P(X <= x).

  • $dist->quantile($p): Inverse CDF value for probability.

  • $dist->sample(): Draw a random value from the distribution according to its weights.

  • $dist->histogram_table($width): Generate a custom-width ASCII chart string.

  • $dist->outcomes: Returns list of Logic::Probabilistic::Distribution::Outcome objects representing individual values and their weights.

Inference Engines

Logic::Probabilistic currently provides three inference engines which are appropriate for different problem scales and models: Exact Enumeration, Forward Rejection Sampling, and Markov Chain Monte Carlo. The default is Exact Enumeration.

Exact Enumeration

Exact enumeration generates all combinations of truth values for the probabilistic atoms (2^k worlds for k probabilistic facts). For each world, it checks whether the evidence and query succeed via Logic::Relational.

query_prob MyModel::burglary,
    evidence => ['alarm'],
    method   => 'exact'
    -> my $result;
    
say $result; # P = 0.4075 (Exact, 32 worlds)

The benefit here is that the result is 100% exact. The downside is the exponential complexity which quickly slows the whole query down for even relatively small k.

Forward Monte Carlo Rejection Sampling

Rejection sampling draws complete independent sample worlds directly from prior distributions. A sample is 'accepted' if it satisfies the evidence. Among accepted samples, the proportion where query succeeds estimates the probability.

query_prob MyModel::burglary,
    evidence => [ MyModel::calls('John'), MyModel::calls('Mary') ],
    method   => 'rejection',
    samples  => 10_000,
    seed     => 42
    -> my $result;
    
say $result; 
# Output: P = 0.4244 [95% CI: 0.3633 .. 0.4879] (10000 samples, rejection)

Every sampling result includes a Wilson score confidence interval computed at a 95% confidence level (or a given custom level).

my ($low, $high) = $result->ci(0.95);
say "95% confidence interval: $low .. $high";

The benefit of rejection sampling is that it can be much faster than exact enumeration, of course trading off between sample size/speed and the final accuracy of the probability estimation. However, the approach is 'embarrassingly parallel', so with the correct setup, high sample sizes can be obtained on many modern CPUs (see below).

An occasional but major con with this approach is that if evidence is rare (e.g. P(Evidence) = 0.001) almost all samples end up being rejected; thereby motivating the next approach.

Markov Chain Monte Carlo

The Metropolis-Hastings Markov Chain Monte Carlo ('MCMC') aims to solve the issue of very low evidence probabilities by taking a random walk only through the subspace of worlds that satisfy the evidence.

query_prob MyModel::burglary,
    evidence => ['alarm'],
    method   => 'mcmc',
    chains   => 4, # 4 independent parallel Markov chains (default)
    samples  => 10_000, # 2500 samples per chain
    burn_in  => 1000,   # warmup transitions discarded before recording
    lag      => 10,     # subsample every 10 transitions to reduce autocorrelation
    seed     => 42
    -> my $result;
    
say $result;
# Output: P = 0.4150 [95% CI: 0.3720 .. 0.4580] (4 chains x 2500 samples, R-hat = 1.01, ESS = 1840, mcmc)

When using the MCMC method, it is important to verify convergence in case of mode-trapping. The following methods are provided for this:

say "Chains:    " . $result->chains;
say "R-hat:     " . $result->r_hat;
say "ESS:       " . $result->ess;
say "Converged: " . ($result->is_converged ? "yes" : "no");

Resource Management

The 2^k worlds that result from k independent probabilistic facts can result in queries that are unsparing to CPU and RAM. Some methods are available for applying constraint.

World Enumeration Cap (max_worlds)

When using the exact inference method, the default limit is 4,096 worlds. (k = 12). Exceeding this threshold causes the engine to display an error message rather than proceeding. If you have the available compute (or time to spare) then change max_worlds as required.

query_prob Model::goal, max_worlds => 65_536 -> my $result;
query_prob Model::goal, max_worlds => 0 -> my $result; # 0 = unlimited

max_worlds can also be configured at the model/program level.

$Model::PROGRAM->max_worlds(16_384);

Timeout Guard (timeout)

Set a maximum execution time in seconds. This is supported across all the inference engines. If execution exceeds the specified duration, it will abort with a timeout message.

query_prob Model::goal, timeout => 5.0 -> my $result;

Parallel Rejection Sampling

Forward Monte Carlo rejection sampling is an embarrassingly parallel algorithm that scales across multiple CPU cores with near-linear speedup. Currently Logic::Probabilistic does not itself provide a parallelization mechanism (for now preferring to leave the specific implementation to the user), but the basic approach is documented below and an example is provided (see examples/parallel_rejection.pl).

  1. Divide the total sample budget across worker processes ($samples / $num_workers).

  2. Ensure each worker is initialized with a different PRNG seed to ensure they each explore independent random worlds.

  3. Collect the counts total_samples, accepted_samples, query_success_count from each worker on finish.

  4. Construct a unified Logic::Probabilistic::Result combining all worker counts.

Managing Probabilistic Facts

In applications like parameter estimation, online sensor updates, reinforcement learning, and belief revision, fact probabilities often need to be altered dynamically without rebuilding the entire model. For managing pfacts use the update_pfact, set_pfact and retract_pfact methods, callable through the program object (the program object for prob_logic ModelName { ... } can be accessed with $ModelName::PROGRAM).

use v5.38;
use Logic::Probabilistic::Syntax;

prob_logic SensorFusion {
    pfact 0.20 => sensor_alarm('cam1');
    pfact 0.15 => sensor_alarm('radar');

    rule intruder {
        sensor_alarm('cam1');
        sensor_alarm('radar');
    }
}

# 1. Query initial state
query_prob SensorFusion::intruder -> my $res1;
say "Initial P(intruder): $res1"; # ~0.0300

# 2. Dynamically update camera sensor reading using $SensorFusion::PROGRAM
$SensorFusion::PROGRAM->update_pfact(0.95, 'sensor_alarm', 'cam1');

# 3. Upsert a new acoustic sensor reading
$SensorFusion::PROGRAM->set_pfact(0.80, 'sensor_alarm', 'acoustic');

# 4. Query again - reflecting the updated sensor probabilities
query_prob SensorFusion::intruder -> my $res2;
say "Updated P(intruder): $res2"; # ~0.1425

Updating with update_pfact

Calling pfact multiple times with the same predicate does not overwrite the earlier probability. Instead, it introduces multiple independent causes. To adjust the probability of an existing pfact, use update_pfact.

# Update probability in-place (supports either order of arguments):
$prog->update_pfact(0.85, 'battery_low', 'drone_1');
# or: $prog->update_pfact('battery_low', 0.85, 'drone_1');

# Or pass a PFact object directly:
my ($pf) = grep { $_->predicate eq 'battery_low' } $prog->all_pfacts;
$prog->update_pfact($pf, 0.90);

Note that update_pfact does not work on a fact declared with fact (a fact is a completely different object, it is not sugar for a pfact with an implied probability of 1).

'Upsert' with set_pfact

When you want to update an existing probabilistic fact if present, or create it if absent, use set_pfact:

# If 'sensor_ok(camera_front)' exists, updates its probability to 0.99.
# If it does not exist, asserts it as a new pfact.
$prog->set_pfact(0.99, 'sensor_ok', 'camera_front');

Retracting with retract_pfact and retract_all_pfacts

# Retract the first matching pfact (returns the removed PFact object, or undef):
my $removed = $prog->retract_pfact('sensor_alert');

# Retract a specific ground atom:
$prog->retract_pfact('node_down', 'alpha');

# Retract all pfacts matching a predicate:
my @removed_all = $prog->retract_all_pfacts('node_down');

Named-argument assertion with assert_pfact

For low-level programmatic creation with full options use assert_pfact.

$prog->assert_pfact(
    probability => 0.75,
    predicate   => 'link_up',
    args        => ['router_a', 'router_b'],
    expires_in  => 60, # Optional TTL in seconds
);

Time-to-live (TTL) and Expiring Facts

Probabilistic facts can be configured with an expiration lifetime (expires_in => $seconds) or absolute epoch (expires_at => $epoch). The inference engine automatically prunes expired facts before evaluating queries, or you can invoke manual cleanup.

# Assert a transient belief valid for only 5 seconds:
$prog->pfact(0.80, 'transient_glitch', { expires_in => 5 });

# Prune any expired facts manually:
my $pruned_count = $prog->cleanup_expired_pfacts();

Managing Categorical Choices

Categorical choices (annotated disjunctions) can also be mutated or removed dynamically using update_pchoice and retract_pchoice.

# Update the probability distribution over outcomes in-place:
$prog->update_pchoice('traffic', [
    [0.60, 'low'],
    [0.40, 'high'],
]);

# Remove a categorical random variable entirely:
$prog->retract_pchoice('traffic');

Model Persistence and Snapshots

Logic::Probabilistic includes native JSON snapshot serialization via Logic::Probabilistic::Serializer, allowing saving and loading from file. A snapshot captures:-

  • Probabilistic facts (pfacts)

    Predicates, ground arguments, probabilities, and TTL expiration timestamps.

  • Categorical choices (pchoices)

    Predicates, base arguments, and outcome probability distributions.

  • Deterministic background facts

    Horn clauses with empty bodies asserted into the relational program.

  • Metadata

    Format version, engine identifier, model version, and export timestamp.

Note: Rules with bodies remain defined in code; the snapshot serializes the factual knowledge base and parameter state.

Saving Snapshots (save_snapshot)

save_snapshot accepts a file path, an open filehandle, or an in-memory scalar reference. Passing pretty = 1> produces human-readable indented JSON.

# 1. Save directly to a JSON file on disk:
$prog->save_snapshot("model_state.json", pretty => 1);

# 2. Save in-memory to a scalar variable:
my $json_string;
$prog->save_snapshot(\$json_string);

# 3. Save to an open filehandle / stream:
open my $fh, '>:encoding(UTF-8)', "state.json" or die $!;
$prog->save_snapshot($fh);
close $fh;

Loading Snapshots (load_snapshot)

load_snapshot restores facts into any existing or newly created program instance.

# Load from a file on disk:
my $loaded_count = $prog->load_snapshot("model_state.json");

# Or load from an in-memory JSON string:
$prog->load_snapshot(\$json_string);

Merge vs. Replace Modes

By default, load_snapshot operates in 'replace' mode, clearing existing facts and choices while preserving compiled rules.

# Replace mode (default): Wipes previous facts/choices and installs snapshot
$prog->load_snapshot("checkpoint.json", mode => 'replace');

# Merge mode: Combines snapshot facts with currently loaded facts
$prog->load_snapshot("supplementary_beliefs.json", mode => 'merge');

Preserving TTLs in Simulations (ttl_mode => 'relative')

When saving facts with expiration lifetimes (expires_in => $secs), storing an absolute timestamp means the fact might expire while the snapshot sits on disk.

To pause and resume simulations without losing remaining time-to-live, use relative TTL mode.

# When reloading, adjust TTLs relative to the current time.
$prog->load_snapshot("checkpoint.json", ttl_mode => 'relative');
# or: $prog->load_snapshot("checkpoint.json", pause_ttl => 1);

Differentiable Logic Programming

Logic::Probabilistic provides support for differentiable logic programming. By treating probabilistic logical deduction as a differentiable function, exact analytical gradients can be computed and backpropagated into gradient-based optimizers and even neural networks, facilitating neurosymbolic systems.

In exact inference mode, pass with_gradients => 1 to compute exact partial derivatives with respect to all underlying Bernoulli facts and categorical choice outcomes. Logic::Probabilistic computes these using boundary-safe prefix-suffix decision products, ensuring gradients evaluate with 100% numerical precision even when p_k = 0.0 or p_k = 1.0 (thereby avoiding division-by-zero errors).

# Query with analytical gradients enabled
my $res;
query_prob Model::failure, with_gradients => 1 -> $res;

# Check whether gradients are populated
if ($res->has_gradients) {
    # Retrieve all gradients as a hashref
    my $grads = $res->gradients; # { 'fault_a' => 0.40, 'fault_b' => 0.60 }

    # Retrieve partial derivative for a specific fact (string name or PFact object)
    my $dp_da = $res->gradient('fault_a');
    say "dP / d(fault_a) = $dp_da";
}

AUTHOR

Matt Johnson, <mjohnson at affectivesilicon.com>

AI

This module was developed with assistance from various AI/LLM, particularly Gemini Flash 3.8.

SEE ALSO

Logic::Relational, Logic::Probabilistic::Syntax, Logic::Probabilistic::Program, Logic::Probabilistic::Distribution, Logic::Probabilistic::Result.

LICENSE AND COPYRIGHT

This software is Copyright (c) 2026 by Matt Johnson.

This is free software, licensed under the GNU General Public License, Version 3.