Skip to main content

PhantomTrace — a mathematical framework where numbers exist in present or absent states with custom operations to include addition, subtraction, multiplication, division, and erasure.

Project description

PhantomTrace

A Python library implementing an experimental mathematical framework where numbers can exist in two states: present or absent. It defines five operations that interact with these states in consistent, rule-based ways.

Zero is redefined: 0 is not emptiness — it's one absence (1(0)). This means every operation has a defined result, including division by zero.

Home: GitHub Repository

Issues: GitHub Issues

Read the paper: Absence Theory

Installation

pip install phantomtrace

Imports

from phantomtrace import n, add, subtract, multiply, divide, erase

The absence_calculator module name is also supported for backward compatibility:

from absence_calculator import n, add, subtract, multiply, divide, erase

Core Concepts

Objects and States

An object is a number that has both a value and a state:

  • Present (default): Written normally, e.g. 5. Present quantities reflect the presence of a given unit of interest. (e.g. if the unit is a cat, then 5 represents 5 cats that are there or in a present state)
  • Absent: Written with (0), e.g. 5(0) — think of it as 5 * 0. Absent quantities reflect the absence of a given unit of interest. (e.g. if the unit is a phone, then 5(0) represents 5 phones that are not currently there but are still considered for computation)

Both states carry magnitude. 5 and 5(0) both have a value of 5 — the state tells you whether it's present or absent, but the magnitude never disappears.

Absence

  • Zero: 0 is not emptiness, it's one absence (1(0) = 1 * 0 = 0)
  • Absence of absence returns to present: 5(0)(0) = 5, and 0(0) = 1

Operations

Operation Symbol Rule
Addition + Expands the amount of objects under consideration. Same state: magnitudes combine. Mixed: unresolved
Subtraction - Contracts the amount of objects under consideration. Same state: magnitudes reduce (can go negative). Mixed: unresolved. void - x = -x (a promise to remove x when there is material creates a temporary expansion — a debt). x - void = x (nothing to remove).
Multiplication * Magnitudes multiply. States combine (present × present = present, absent × present = absent, absent × absent = present)
Division / Magnitudes divide. States combine same as multiplication. Division by 0 is defined
Erasure erased Same state required. Remainder keeps state, erased portion flips state. Over-erasure creates erased excess

Number Creation

Use the n() shorthand to create numbers quickly:

from phantomtrace import n

n(5)       # → 5 (present)
n(5)(0)    # → 5(0) (absent) — closest to writing 5(0) directly
n(5)(1)    # → 5 (stays present)
n(3)(5)    # → 15 (multiplier — 3 × 5)
n(10)(0)   # → 10(0) (absent)

# Build vectors naturally
vec = [n(10)(0), n(20), n(30)(0), n(40), n(50)(0)]
# → [10(0), 20, 30(0), 40, 50(0)]

You can also use the full form: AbsentNumber(value, absence_level).

Quick Start

from phantomtrace import n, add, subtract, multiply, divide, erase, format_result

# Create numbers — present (default) or absent
five = n(5)             # 5 (present)
three_absent = n(3)(0)  # 3(0) (absent)

# Addition — same state combines, mixed state is unresolved
result = add(n(5), n(3))
print(result)  # 8

# Subtraction — equal values cancel to void
result = subtract(n(7), n(7))
print(result)  # void

# Subtraction — can go negative
result = subtract(n(3)(0), n(5)(0))
print(result)  # -2(0) — negative absence is its own thing

# Multiplication — states combine (like XOR)
result = multiply(n(5)(0), n(3))
print(result)  # 15(0)

# Erasure — flips the state of the erased portion
result = erase(n(5), n(3))
print(result)  # 2 + 3(0)

# Over-erasure — excess becomes erased debt
result = erase(n(7), n(10))
print(result)  # 7(0) + erased 3

# Resolve erased excess by adding
resolved = add(result, n(3))
print(resolved)  # 10(0)

# Division by zero — defined (0 is one absence)
result = divide(n(10), n(1)(0))
print(result)  # 10(0)

All operations accept plain Python integers — they are automatically treated as present numbers:

from phantomtrace import add, subtract, multiply, divide, erase

add(n(5), 3)      # → 8
subtract(10, 3)   # → 7
multiply(n(5), 3) # → 15
divide(10, 2)     # → 5
erase(5, 3)       # → 2 + 3(0)

Void

Void is not a number. It is not zero, not an empty set, not a placeholder — it is the complete absence of any quantity under consideration. When void appears, it means there is no domain left to operate on.

Where void comes from: Subtraction is erasure with forgetting. Erasure removes a quantity but keeps its absence visible — the erased portion flips state and remains in the result. Subtraction goes one step further and forgets the absence too, removing it entirely from consideration. When equal quantities are subtracted, the entire domain of consideration collapses. Nothing is left — not even an absence. That is void.

subtract(n(5), n(5))   # → void   (domain collapses — nothing left to consider)
subtract(n(5), n(3))   # → 2      (domain shrinks but remains)
erase(n(5), n(5))      # → 5(0)   (erasure: the absence is still there, just flipped)

Why you cannot truly add or subtract void: Since void is not a number, adding it to or removing it from a quantity makes no strict sense - all operations should work on quantity, not the absence of it. For the sake of an 'invisible' element, void is still allowed in subtraction and addition.

add(n(5), VOID)        # → 5   (library returns the real quantity)
subtract(n(7), VOID)   # → 7   (nothing to remove)
subtract(VOID, n(5))   # → -5  (promise to remove when domain exists creates a debt)

Multiplication, division, and erasure raise NoQuantityError when void is involved. Since multiplication and division are repetivie operations they need quantity on which to act. Erasure cannot process void because it cant change the state of something that isnt even an object to begin with.

Using the Expression Solver

from phantomtrace import solve, format_result

print(format_result(solve("5 + 3")))           # 8
print(format_result(solve("5(0) + 3(0)")))     # 8(0)
print(format_result(solve("7 - 7")))           # void
print(format_result(solve("5(0) * 3")))        # 15(0)
print(format_result(solve("5 erased 3")))      # 2 + 3(0)
print(format_result(solve("7 erased 10")))     # 7(0) + erased 3
print(format_result(solve("5(0)(0)")))         # 5 (double absence = present)

# Parenthesized expressions
print(format_result(solve("(1 + 5(0)) erased 1")))  # 6(0)

# Zero operations
print(format_result(solve("0 + 0")))           # 2(0) (two absences)
print(format_result(solve("0 * 0")))           # 1 (absence of absence = presence)
print(format_result(solve("10 * 0")))          # 10(0)
print(format_result(solve("10 / 0")))          # 10(0)

Interactive Calculator

After installing, run the interactive calculator from the command line:

phantomtrace

Or as a Python module:

python -m absence_calculator

This gives you a calc >> prompt where you can type expressions and see results.

Erasure

Erasure is subtraction without forgetting the quantity removed. There are still the same number of objects present for computation before and after the application of erasure. Similarly to subtraction, over-erasure produces a debt-like quantity.

When you erase more than the total, the result carries erased excess (erasure debt):

  • 7 erased 10 = 7(0) + erased 3 — all 7 flip state, 3 excess erasure persists
  • Adding resolves excess: (7(0) + erased 3) + 3 = 10(0)

erased as a shorthand operator

erased doubles as an infix operator. Use |erased| between two values for a compact, readable shorthand — it means the same as calling erase(x, y):

from phantomtrace import n, erased

n(7) |erased| n(3)    # → 4 + 3(0)   (same as erase(n(7), n(3)))
n(3) |erased| n(7)    # → 3(0) + erased 4   (over-erasure)
7    |erased| n(3)    # → 4 + 3(0)   (plain ints work too)
n(7) |erased| 3       # → 4 + 3(0)

For fractional, double, or inverse erasure levels, use erased**level as the operator:

from phantomtrace import n, erased

n(7) |erased**2|   n(3)    # → 7 + erased erased 3   (two levels of erasure on the right)
n(7) |erased**0.5| n(3)    # → 7 + erased^0.5 3
n(7) |erased**-1|  n(3)    # → 4 + erased^-1 3(0)
n(7) |erased**1|   n(3)    # → 4 + 3(0)              (same as plain |erased|)

erased**level can also be called directly as a function:

(erased**2)(n(5))      # → erased erased 5   (same as erased_n(n(5), 2))
(erased**0.5)(n(5))    # → erased^0.5 5

erased(x) still works exactly as before — all these forms are just shorthand on top.

Erasure and Subtraction: erased() and negative()

Just like subtraction can be thought of as adding a negative (x - y = x + (-y)), erasure can be thought of as adding an erased number: erase(x, y) = x + erased(y). The erased() function creates an erased number, and the erase() function is shorthand for adding that erased state.

from phantomtrace import n, erase, erased, negative, add

# erased() takes one input — the number you want to apply an erased state to
erased(n(5))       # → erased 5
erased(n(5)(0))    # → erased 5(0)

# erase(x, y) is equivalent to add(x, erased(y))
# Just like subtract(x, y) is equivalent to add(x, negative(y))
erase(n(5), n(3))              # → 2 + 3(0)
add(n(5), erased(n(3)))        # → 2 + 3(0) (same thing)

# Same state + same erasure level combine when added
add(erased(n(3)), erased(n(7)))        # → erased 10
add(erased(n(3)(0)), erased(n(7)(0))) # → erased 10(0)

# Adjacent levels resolve — erased(x) can erase x because x is there to erase
add(erased(n(5)), n(5))        # → 5(0)
add(erased(n(5)(0)), n(5)(0))  # → 5

# Two levels apart can't resolve — erased erased 5 needs erased 5, not plain 5
add(erased(erased(n(5))), n(5))          # → 5 + erased erased 5 (stays separate)
add(erased(erased(n(5))), erased(n(5)))  # → erased 5(0) (adjacent, resolves)

# negative() creates a negative number — same as normal math
negative(n(5))     # → -5
negative(n(5)(0))  # → -5(0)

# Both work on tensors element-wise
erased([n(1), n(2), n(3)])   # → [erased 1, erased 2, erased 3]
negative([n(1), n(2), n(3)]) # → [-1, -2, -3]

Erasure Levels — erased_n(x, n)

erased_n(x, n) applies exactly n levels of erasure to x in a single call. Erasure levels are real numbers — integer, fractional, or negative.

from phantomtrace import erased_n, multiply, divide, n

# Integer levels — same as calling erased() n times
erased_n(5, 1)    # → erased 5          (same as erased(5))
erased_n(5, 2)    # → erased erased 5   (same as erased(erased(5)))
erased_n(5, 0)    # → 5                 (zero erasure = identity, no change)

# Negative levels — (erased^-1(x) = x/erased(1))
erased_n(5, -1)   # → erased^-1 5

# Fractional levels
erased_n(1, 0.5)  # → erased^0.5 1

Multiplication and division: Erasure levels add during multiplication and subtract during division — the same rule that makes absent × absent = present:

# Half + half = full
h = erased_n(1, 0.5)
multiply(h, h)              # → erased 1   (0.5 + 0.5 = 1)

# Positive and negative cancel — result is a plain number, no erasure
e_pos = erased_n(3, 1)      # erased 3
e_neg = erased_n(3, -1)     # erased^-1 3
multiply(e_pos, e_neg)      # → 9   (1 + (-1) = 0, erasure gone, 3 × 3 = 9)

# 1/erased(x) produces erased^-1
divide(n(1), erased_n(1, 1))  # → erased^-1 1
# Multiplying back cancels:
multiply(divide(n(1), erased_n(1,1)), erased_n(1,1))  # → 1

# erased applied to erased^-1 cancels back to the plain number
erased(erased_n(5, -1))     # → 5   (level -1 + 1 = 0, erasure disappears)

When an erasure level reaches exactly 0 through any combination of operations, the result automatically returns to a plain AbsentNumber.

Addition and subtraction — same level required, except at adjacent integer levels:

Two erased quantities at the same integer or fractional level can only combine when their level matches exactly:

from phantomtrace import erased_n, add, subtract

# Same level, same state — values combine
add(erased_n(7, 6), erased_n(8, 6))    # → erased^6 15
subtract(erased_n(8, 6), erased_n(5, 6))  # → erased^6 3
subtract(erased_n(7, 6), erased_n(7, 6))  # → void

# Same level, different states — stays unresolved (levels match but states don't)
add(erased_n(9, 0.5), erased_n(n(5)(0), 0.5))  # → erased^0.5 9 + erased^0.5 5(0)

# Different levels — stays unresolved
add(erased_n(5, 0.5), erased_n(6, -1))  # → erased^-1 6 + erased^0.5 5

Adjacent levels interact for any real-valued erasure level — exactly like how erased(5) + 5 = 5(0) at the base level, this generalises to any two quantities whose erasure levels differ by exactly 1. The higher level erases into the lower, making the lower-level value absent:

# Integer levels
add(erased_n(5, 2), erased_n(5, 1))        # → erased 5(0)
add(erased_n(7, 3), erased_n(7, 2))        # → erased erased 7(0)
add(erased_n(5, -1), n(5))                 # → erased^-1 5(0)
add(erased_n(7, -2), erased_n(7, -1))      # → erased^-2 7(0)

# Fractional levels — same rule applies
add(erased_n(5, 0.5), erased_n(5, -0.5))  # → erased^-0.5 5(0)
add(erased_n(5, 1.5), erased_n(5, 0.5))   # → erased^0.5 5(0)
add(erased_n(7, 2.5), erased_n(7, 1.5))   # → erased^1.5 7(0)

# Over-erasure works the same at any level
add(erased_n(3, 2), erased_n(5, 1))       # → erased 2 + erased 3(0)
add(erased_n(3, -1), n(5))                # → 2 + erased^-1 3(0)

The rule applies only when both quantities work on the same value x and differ by exactly 1. A gap of 2 or more stays unresolved:

add(erased_n(5, 2), n(5))                  # → 5 + erased erased 5   (gap = 2, unresolved)
add(erased_n(5, 0.5), erased_n(6, -1))     # → erased^-1 6 + erased^0.5 5  (gap = 1.5, unresolved)

Layered Erasure

Erasure can be layered to any depth — you can erase erased quantities from other erased quantities. The same erasure rules apply: same state required, remainder keeps state, erased portion flips state.

Important: Double erasure is NOT like double negatives. A double negative gives you a positive (-(-5) = 5). But erasing an erased number gives you the absence of an erased number — and that only happens when an erased number of the same kind is actually present to be erased. The absence of an erased number is its own distinct thing, not a return to the original number. Erasure and subtraction are analogous (both remove quantity), but they behave differently when layered.

Each layer tracks its depth internally, so only quantities at the same erasure depth and same absence state can interact.

from phantomtrace import n, erase, erased

# erase(erased(5), erased(3)) — erase "erased 3" from "erased 5"
# Remainder: erased 2 (unchanged). Erased portion: erased 3(0) — the absence of "erased 3"
erase(erased(n(5)), erased(n(3)))  # → erased 2 + erased 3(0)

# Full erasure — everything becomes the absence of its erased state
erase(erased(n(5)), erased(n(5)))  # → erased 5(0)

# Over-erasure — same rules apply
erase(erased(n(3)), erased(n(5)))  # → erased 3(0) + erased 2

# Works on absent-state erased quantities too
erase(erased(n(5)(0)), erased(n(3)(0)))  # → erased 2(0) + erased 3

# Mixed-state erased quantities can't erase each other
erase(erased(n(5)), erased(n(3)(0)))     # → unresolved (different states)

# Deeper layers work the same way
erased(erased(n(5)))             # → erased erased 5
erase(erased(erased(n(5))), erased(erased(n(3))))
# → erased erased 2 + erased erased 3(0)

# Different depths can't interact
erase(erased(erased(n(5))), erased(n(5)))
# → unresolved (different erasure depths)

Compound Expressions

Operations work on unresolved expressions as inputs:

  • (1 + 5(0)) erased 1 = 6(0) — erases the present part, combining with the absent part

All five operations distribute over Unresolved expressions:

from phantomtrace import n, add, subtract, multiply, divide

u = subtract(n(7), n(5)(0))    # → 7 - 5(0) (unresolved — different states)

add(u, 7)                      # → 14 - 5(0)
add(n(8)(0), u)                # → 7 + 3(0)
subtract(u, 2)                 # → 5 - 5(0)
multiply(u, n(5)(0))           # → 35(0) - 25
divide(u, n(5)(0))             # → 1.4(0) - 1
divide(n(5)(0), u)             # → 5(0) / 7 - 5(0)

multiply(divide(n(5)(0), u), 2)  # → 10(0) / 7 - 5(0)

Multiplication distributes because it is repeated addition. Division can be thought of as repeated subtraction up until void. Read Absence Theory for more information.

Result Types

  • AbsentNumber: A number with a state (present or absent)
  • Void: Complete cancellation — not zero, but the absence of any quantity under consideration
  • ErasureResult: Two parts — remainder (keeps state) and erased portion (flipped state)
  • ErasedExcess: Excess erasure debt that persists until resolved. Created directly with erased()
  • Unresolved: An expression that cannot be simplified (e.g., adding present + absent). Stores the full operation so it can be operated on in future computations

Trace Function

trace() is an absent-aware lambda — it evaluates a function over a range of values, producing a vector of results. The range can be present or absent, and all operations work naturally within the trace.

Trace Ordering

Numbers of different states (present vs absent) are the same magnitude, just different state — they cannot be ordered against each other by default. Same-state ranges iterate naturally by magnitude:

from phantomtrace import n, trace, multiply

trace(lambda x: multiply(x, x), n(1), n(5))
# → [1, 4, 9, 16, 25] — present ascending

trace(lambda x: multiply(x, x), n(5)(0), n(2)(0))
# → [25, 16, 9, 4] — absent descending

Cross-state ranges require a user-defined ordering:

from phantomtrace import n, trace, present, absent, largest, ordering

o = ordering(largest(present()), largest(absent()))

trace(lambda x: x, n(3), n(3)(0), order=o)
# → [3, 2, 1, 0, 2(0), 3(0)]
# Path: present descending → boundary → absent ascending

trace(lambda x: x, n(5)(0), n(5), order=o)
# → [5(0), 4(0), 3(0), 2(0), 0, 1, 2, 3, 4, 5]

Without an ordering, cross-state trace raises an error — the framework does not assume which state comes first.

Basic Traces

from phantomtrace import n, trace, multiply, add, erase, divide, subtract

# x² over an absent range
trace(lambda x: multiply(x, x), n(5)(0), n(2)(0))
# → [25, 16, 9, 4]   (absent × absent = present)

# x erased x — flips every value to the opposite state
trace(lambda x: erase(x, x), n(1), n(5))
# → [1(0), 2(0), 3(0), 4(0), 5(0)]

# x + x over a present range
trace(lambda x: add(x, x), n(1), n(4))
# → [2, 4, 6, 8]

# x - 1 over a range
trace(lambda x: subtract(x, n(1)), n(3), n(5))
# → [2, 3, 4]

# x / 2 over a range
trace(lambda x: divide(x, n(2)), n(2), n(6))
# → [1, 1.5, 2, 2.5, 3]

# Mixed operations: x² + x over absent range
trace(lambda x: add(multiply(x, x), x), n(1)(0), n(3)(0))
# → [1 + 0, 4 + 2(0), 9 + 3(0)]
# x² is present (absent × absent), x is absent → unresolved at each position

Unbound and Partial Traces

You can create a trace without specifying the range and bind it later. bind() returns a new result and does not modify the original trace:

# Unbound — define the function now, bind the range later
t = trace(lambda x: multiply(x, x))
print(t)              # trace(unbound)
t(n(5))               # → 25 (call it like a function)
result = t.bind(n(1), n(5))  # → [1, 4, 9, 16, 25]

# Partially bound — set start now, end later
t2 = trace(lambda x: add(x, n(10)), start=n(1))
result = t2.bind(n(3))       # → [11, 12, 13]
result = t2.bind(end=n(3))   # → [11, 12, 13]

Void Rejection

Trace rejects void ranges — void means no number over which to operate:

from phantomtrace import VOID

trace(lambda x: x, VOID, n(5))
# → ValueError: Cannot trace over void — void is no calculation

Builder Module

The builder module lets you define your own mathematical domains with custom states, state transitions, and operation rules:

from phantomtrace import StateSpace

space = StateSpace("quantum")
superposed = space.add_state("superposed")
collapsed  = space.add_state("collapsed")
entangled  = space.add_state("entangled")

space.add_transition("measure", "superposed", "collapsed")
space.add_transition("entangle", "collapsed", "entangled")

space.add_rule("add", same_state="combine", mixed_state="unresolved")
space.add_rule("multiply", state_combination={
    ("superposed", "superposed"): "collapsed",
    ("superposed", "collapsed"): "entangled",
    ("collapsed",  "collapsed"): "collapsed",
})

x = space.number(5, "superposed")
y = space.number(3, "superposed")
z = space.number(7, "collapsed")

space.add(x, y)           # → 8[superposed]
space.add(x, z)           # → unresolved
space.multiply(x, y)      # → 15[collapsed]
space.multiply(x, z)      # → 35[entangled]
space.transition("measure", x)  # → 5[collapsed]

The existing present/absent system can be expressed as a StateSpace:

from phantomtrace import presence_absence_space

pa = presence_absence_space()
p5 = pa.number(5, "present")
a3 = pa.number(3, "absent")
pa.multiply(p5, a3)  # → 15[absent]
pa.multiply(a3, a3)  # → 9[present] (absence of absence = presence)

Tensor Creation

tensor() creates multi-dimensional tensors — nested lists of AbsentNumbers at any depth. Vectors are rank 1, matrices are rank 2, and you can go as deep as needed. Every element always retains both its value and its state — nothing is ever removed, only toggled.

from phantomtrace import tensor, n

# Vector (rank 1) — 5 elements, all absent
v = tensor(5, fill='absent')
# → [1(0), 2(0), 3(0), 4(0), 5(0)]

# Matrix (rank 2) — 3 rows of 4 elements, all present
m = tensor((3, 4), fill='present')
# → [[1, 2, 3, 4],
#    [1, 2, 3, 4],
#    [1, 2, 3, 4]]

# 3D Tensor (rank 3)
t = tensor((2, 3, 4), fill='absent')

# 4D Tensor (rank 4)
t4 = tensor((2, 2, 3, 5))

Seed — State Randomization

The seed parameter randomizes which positions are present vs absent. Values stay sequential (position = identity) — only states change. Closer seeds produce more similar patterns, with adjacent seeds differing by exactly one position.

r0 = tensor(5, seed=0)
# → [1(0), 2(0), 3(0), 4(0), 5(0)]  (seed 0 = all absent)

r3 = tensor(5, seed=3)
# → 3 positions present, 2 absent (deterministic)

r4 = tensor(5, seed=4)
# → 4 positions present — differs from seed 3 by exactly 1 position

r5 = tensor(5, seed=5)
# → [1, 2, 3, 4, 5]  (seed = size = all present)

# Same seed always gives the same pattern
tensor(5, seed=3) == tensor(5, seed=3)  # True

# Works on matrices and higher — ordering spans the entire tensor
m = tensor((3, 4), seed=6)
# 6 of 12 positions are present, rest absent

Inspecting Tensors

from phantomtrace import toggle, n

toggle.rank(n(5))                              # → 0 (scalar)
toggle.rank([n(1), n(2)])                      # → 1 (vector)
toggle.rank([[n(1), n(2)], [n(3), n(4)]])      # → 2 (matrix)
toggle.rank(tensor((2, 3, 4)))                 # → 3 (3D tensor)

toggle.shape(tensor((3, 4)))                   # → (3, 4)
toggle.shape(tensor((2, 3, 4)))                # → (2, 3, 4)

Tensor Operations

All calculator operations work on tensors — add, subtract, multiply, divide, and erase. Both inputs must have the same shape. Operations are applied element-by-element at every depth.

from phantomtrace import n, add, subtract, multiply, divide, erase

# Addition — same state combines, mixed is unresolved
add([n(7), n(8)(0), n(10)], [n(4), n(3), n(7)(0)])
# → [11, 8(0) + 3, 10 + 7(0)]

# Subtraction — equal values cancel to void
subtract([n(7), n(5), n(10)(0)], [n(3), n(5), n(4)(0)])
# → [4, void, 6(0)]

# Multiplication — states combine (absent × absent = present)
multiply([n(3), n(4)(0), n(2)(0)], [n(5), n(3), n(6)(0)])
# → [15, 12(0), 12]

# Division — magnitudes divide, states combine
divide([n(10), n(9)(0), n(8)], [n(2), n(3)(0), n(4)])
# → [5, 3, 2]

# Erasure — flips state of erased portion at each position
erase([n(7), n(5), n(3)], [n(3), n(5), n(1)])
# → [4 + 3(0), 5(0), 2 + 0]

# All operations work on matrices and higher-rank tensors
add([[n(1), n(2)], [n(3), n(4)]], [[n(5), n(6)], [n(7), n(8)]])
# → [[6, 8], [10, 12]]

Special Functions

Combine

combine() counts present vs absent at each position, ignoring scalar values. Each element becomes 1 (if present) or 1(0) (if absent), then those are added together. Void elements contribute nothing.

from phantomtrace import n, combine, VOID

# Mixed states — each position has one present and one absent
combine([n(1), n(2)(0), n(3), n(4)(0)],
        [n(0), n(2),    n(3)(0), n(4)])
# → [1 + 0, 0 + 1, 1 + 0, 0 + 1]

# Both present at every position
combine([n(5), n(3)], [n(2), n(7)])
# → [2, 2]

# Both absent at every position
combine([n(5)(0), n(3)(0)], [n(2)(0), n(7)(0)])
# → [2(0), 2(0)]

# Combine with void — counts only the non-void side
combine([n(5), n(4)(0), n(3), n(2)(0)], [VOID, VOID, VOID, VOID])
# → [1, 0, 1, 0]

# Void + void = void
combine([VOID, VOID], [VOID, VOID])
# → [void, void]

Compare

compare() measures the shift in present vs absent from tensor 1 to tensor 2. Most useful on already-combined tensors where each position has the same total magnitude split between present and absent.

from phantomtrace import n, combine, compare

c1 = [n(1), n(2), n(3), n(4)(0)]
c2 = [n(5), n(6), n(7), n(8)]
state1 = combine(c1, c2)
# → [2, 2, 2, 0 + 1]

c3 = [n(1)(0), n(2),    n(3)(0), n(4)]
c4 = [n(5),    n(6)(0), n(7)(0), n(8)]
state2 = combine(c3, c4)
# → [0 + 1, 1 + 0, 2(0), 2]

compare(state1, state2)
# → [0, 0, 2(0), 1]

# No change returns void
same1 = combine([n(1), n(2)(0)], [n(3)(0), n(4)])
same2 = combine([n(5)(0), n(6)], [n(7), n(8)(0)])
compare(same1, same2)
# → [void, void]

Join

join() concatenates two lists into one, preserving all elements and their states:

from phantomtrace import n, join, VOID

a = [n(5)(0), n(7), VOID, n(9)]
b = [n(6), n(7)(0), n(4)]

join(a, b)
# → [5(0), 7, void, 9, 6, 7(0), 4]

Toggle Module

The toggle module flips states of elements in vectors, matrices, and tensors using pattern-based index selection.

Core Toggle Operations

  • toggle.where(pattern, range, data) — flip elements at pattern-computed indices
  • toggle.exclude(pattern, range, data) — flip everything except pattern-computed indices
  • toggle.all(data) — flip every element at any depth

The pattern is a function (or string expression) evaluated across all whole numbers. The range is an output filter — only results that fall within (start, end) become target indices.

Vectors — Present

from phantomtrace import toggle, n

vec = [10, 20, 30, 40, 50]

# x*2 produces 0, 2, 4, 6, 8... — range (0, 4) keeps 0 through 4
# Hits indices 0, 2, 4 (even positions)
toggle.where(lambda x: x * 2, (0, 4), vec)
# → [10(0), 20, 30(0), 40, 50(0)]

toggle.exclude(lambda x: x * 2, (0, 4), vec)
# → [10, 20(0), 30, 40(0), 50]

toggle.all(vec)
# → [10(0), 20(0), 30(0), 40(0), 50(0)]

Vectors — Absent

vec = [n(10)(0), n(20)(0), n(30)(0), n(40)(0), n(50)(0)]

toggle.where(lambda x: x * 2, (0, 4), vec)
# → [10, 20(0), 30, 40(0), 50]

toggle.exclude(lambda x: x * 2, (0, 4), vec)
# → [10(0), 20, 30(0), 40, 50(0)]

toggle.all(vec)
# → [10, 20, 30, 40, 50]

Vectors — Mixed

vec = [n(10), n(20)(0), n(30), n(40)(0), n(50)]

toggle.where(lambda x: x * 2, (0, 4), vec)
# → [10(0), 20(0), 30(0), 40(0), 50(0)]

toggle.exclude(lambda x: x * 2, (0, 4), vec)
# → [10, 20, 30, 40, 50]

toggle.all(vec)
# → [10(0), 20, 30(0), 40, 50(0)]

String Patterns and Single Index

# String pattern — "x^2" computes target indices
toggle.where("x^2", (1, 4), [4, 7, 19, 22, 26])
# → [4, 7(0), 19, 22, 26(0)]   indices 1 (1²) and 4 (2²) toggled

# Single index — use pattern "x" with range (i, i)
toggle.where("x", (2, 2), [10, 20, 30, 40, 50])
# → [10, 20, 30(0), 40, 50]

Matrices — Present

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

toggle.all(matrix)
# → [[1(0), 2(0), 3(0)],
#    [4(0), 5(0), 6(0)],
#    [7(0), 8(0), 9(0)]]

toggle.where("x", (0, 0), matrix)
# → [[1(0), 2, 3],
#    [4(0), 5, 6],
#    [7(0), 8, 9]]

toggle.exclude("x", (0, 0), matrix)
# → [[1, 2(0), 3(0)],
#    [4, 5(0), 6(0)],
#    [7, 8(0), 9(0)]]

Matrices — Absent

matrix = [[n(1)(0), n(2)(0)], [n(3)(0), n(4)(0)]]

toggle.all(matrix)
# → [[1, 2],
#    [3, 4]]

Matrices — Mixed

matrix = [[n(10)(0), n(20), n(30)(0)],
          [n(40),    n(50)(0), n(60)]]

toggle.where("x", (1, 1), matrix)
# → [[10(0), 20(0), 30(0)],
#    [40, 50, 60]]

toggle.all(matrix)
# → [[10, 20(0), 30],
#    [40(0), 50, 60(0)]]

Toggling at Any Depth

toggle.all() works at every depth — flips every element regardless of how deeply nested:

from phantomtrace import tensor, toggle

t = tensor((2, 3, 4), fill='present')
t_flipped = toggle.all(t)
# Every element in the entire 2×3×4 structure is now absent

Axis-Aware Toggling

where() and exclude() accept an axis parameter to control which level toggling happens at:

from phantomtrace import tensor, toggle

# Matrix 3×5, all absent
m = tensor((3, 5), fill='absent')

# Identity function, range (0, 2) — toggles first 3 columns in each row
result = toggle.where(lambda x: x, (0, 2), m, axis=-1)
# Row 0: P P P _ _
# Row 1: P P P _ _
# Row 2: P P P _ _

# x*2, range (0, 4) — hits even indices only
result = toggle.where(lambda x: x * 2, (0, 4), m, axis=-1)
# Row 0: P _ P _ P
# Row 1: P _ P _ P
# Row 2: P _ P _ P

# 3D tensor — reaches the deepest vectors
t = tensor((2, 2, 4), fill='absent')
result = toggle.where(lambda x: x, (1, 2), t, axis=-1)
# Toggles indices 1 and 2 in every innermost vector:
# [0][0]: _ P P _
# [0][1]: _ P P _
# [1][0]: _ P P _
# [1][1]: _ P P _

Selecting Slices

toggle.select() pulls out a sub-structure along any axis. The result is one rank lower:

from phantomtrace import n

m = [[n(1), n(2), n(3)],
     [n(4), n(5), n(6)],
     [n(7), n(8), n(9)]]

toggle.select(m, axis=0, index=1)  # → [4, 5, 6]   (row 1)
toggle.select(m, axis=1, index=2)  # → [3, 6, 9]   (column 2)

Counting and Querying

from phantomtrace import toggle, n, tensor

v = [n(1), n(2)(0), n(3), n(4)(0), n(5)]

toggle.count_present(v)  # → 3
toggle.where_present(v)  # → (array([0, 2, 4]),)

The .pt Language

PhantomTrace ships with its own small language — .pt files — that compile to Python before running. It lets you write absence calculus in natural notation.

Running a .pt file

python -m phantomtrace myfile.pt

State notation

Write absent numbers directly without n():

You write Compiled Python
5(0) n(5)(0) — absent 5
7.5(0) n(7.5)(0) — absent 7.5
5(0)(0) n(5)(0)(0) — double absence = present 5
void VOID
Everything else Unchanged Python

erased as a keyword

In .pt files, erased works as a natural infix keyword — no pipes needed:

7 erased 3          # → 4 + 3(0)   same as erase(7, 3)
10(0) erased 4(0)   # → 6(0) + 4
7 erased^2 3        # → 7 + erased erased 3
7 erased^0.5 3      # → 7 + erased^0.5 3

erased(x) (function call form) still works unchanged.

obj — named values and constructors

Bind a value to a name, or define a factory function. obj and defobj are identical — use whichever reads more naturally:

# Constant — give a meaningful name to a value
obj electron: 1(0)
obj proton: 1
obj scale: 2

print(electron)                       # 1(0)
print(multiply(electron, electron))   # 1  — absent × absent = present

# Factory — define a constructor
obj particle(charge, mass): multiply(charge, mass)

print(particle(electron, n(9)))       # 9(0)

op — custom operations

Define a new operation and call it by name. op and defop are identical — use whichever reads more naturally:

# Multi-line form
op fuse(x, y):
    return add(multiply(x, n(2)), y)

print(3 fuse 4)              # 10
print(electron fuse proton)  # 1 + 2(0)

# Single-line form
op overlap(x, y): erase(x, y)

print(7 overlap 3)           # 4 + 3(0)

The body has full access to all PhantomTrace functions. .pt transforms (erased keyword, state notation) also apply inside the body.

Pattern-based op — compact shorthand

For numeric operations you can write the logic as a pattern instead of a full function body. A pattern is a sequence of operators and values that describes what the operation does step by step:

obj fw: 18

op fried:  *fw*0+fw      # one-sided: fried(x)    = x*18*0 + 18 = 18
op fried2: fw*0+fw*      # two-sided: fried2(L,R) = fw*0 + fw*R = 18*R
op spotty: *8 erased 7+4 # one-sided: spotty(x)   = (x*8 erased 7) + 4

How the pattern determines arity:

  • Starts with an operator (*, +, -, /) → one-sided (left operand fills the first slot)
  • Ends with an operator → two-sided (right operand fills the final slot)
  • Starts and ends with values → one-sided (output is fully determined by the pattern)

Three calling styles

Once defined, an op is called with spaces rather than parentheses. The style available depends on arity:

Two-sided op — called as an infix keyword:

5 fuse 3          # fuse(5, 3)      — infix
fuse(5, 3)        # function call always works too

One-sided op — called with the value on either side:

fried 5           # fried(5)        — prefix  (op then value)
5 fried           # fried(5)        — postfix (value then op)
fried(5)          # function call always works

Both prefix and postfix call the same function. Prefix transforms to a function call internally, so it has the highest possible precedence — 2 * fried 5 correctly means 2 * fried(5).

For complex expressions as argument, use explicit parentheses: fried(5 + 3).

Op-in-op composition

An op pattern can reference previously-defined ops by name. Adjacent op names compose left-to-right:

obj fw: 18
op fried:  *fw*0+fw       # one-sided: always returns 18
op fried2: fw*0+fw*       # two-sided: returns 18*R

op cheese: fried*fried2   # cheese(L, R) = fried(L) * fried2(R)

print(fw cheese fw)       # fried(18) * fried2(18) = 18 * 324 = 5832

Ops must be defined before they are referenced in a pattern.

Error handling

Using an op the wrong way raises OperationUseError with a clear message:

from phantomtrace.errors import OperationUseError

# 'fuse' is two-sided — using it one-sided raises an error
fuse 5    # OperationUseError: 'fuse' needs a value on both sides

Inline from Python

from phantomtrace import pt_exec

pt_exec("""
obj electron: 1(0)
op annihilate(x, y): subtract(x, y)
print(electron annihilate electron)  # void
""")

Inspect generated Python

from phantomtrace import transform
print(transform("7 erased 3"))
# from phantomtrace import *
# ...
# 7 |erased| n(3)(0)  ← what the compiler actually produces

Comments are preserved. Existing Python (n(5)(0), print(0), erased(5)) is left untouched.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

phantomtrace-1.0.6.tar.gz (67.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

phantomtrace-1.0.6-py3-none-any.whl (48.4 kB view details)

Uploaded Python 3

File details

Details for the file phantomtrace-1.0.6.tar.gz.

File metadata

  • Download URL: phantomtrace-1.0.6.tar.gz
  • Upload date:
  • Size: 67.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for phantomtrace-1.0.6.tar.gz
Algorithm Hash digest
SHA256 868d7c96fea7a518db05033a175dbb1b73b2da4cced1c1f04892c36b7579bbd1
MD5 69c2124e53ca932b14716f2fd0b0ada6
BLAKE2b-256 0085bd6d67bf71720d2fcc285089fcf89781eeec7192eee06eb56372bc07db05

See more details on using hashes here.

File details

Details for the file phantomtrace-1.0.6-py3-none-any.whl.

File metadata

  • Download URL: phantomtrace-1.0.6-py3-none-any.whl
  • Upload date:
  • Size: 48.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for phantomtrace-1.0.6-py3-none-any.whl
Algorithm Hash digest
SHA256 a8c94d1a4f1de4535a81af6437100cc390bf850cf0f16e835f39440c0475f3a2
MD5 aef1fd4bd2de756e26acfb4f841191cf
BLAKE2b-256 2052c15a4a0488d37a7d3734ac43fc738f1bdddc0b80cdf908998a23ff899a2c

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page