A long food-generation chain can be easy to analyse when every required molecule has one supplier. Alternative suppliers create the branching choices. This example computes the complete family of minimal autocatalytic reaction sets and shows when independent modules avoid unnecessary combinations.

Four irreducible RAFs each include one of reactions zero and one, one of two and three, and both gates four and five.
The paper's complete six-reaction worked catalogue, computed by supplier resolution and original-system validation. Ones indicate membership. Both gates occur in every irreducible RAF; each alternative producer occurs in two.
Independent modules require exponentially many global supplier combinations but only linearly many local resolutions; coupled gates retain exponential growth.
Exact operation counters for the same controlled families. Global independent-module counts and coupled-gate counts overlap at 2 to the size power; component enumeration of independent modules uses twice the size. The logarithmic vertical axis counts resolutions, not elapsed time.

A RAF is a nonempty set whose reactants can be generated from food and whose reactions each have an available catalyst. An irreducible RAF has no smaller RAF inside it. Catalysts are checked after food closure; they do not gate the closure steps. This is a structural definition, with no concentration or kinetic assumptions.

The worked system has two alternative producers of x1x_1 (reactions 0 and 1), two of x2x_2 (2 and 3), and two gates (4 and 5) that build the shared catalyst z2z_2. Each of the four rows in the first figure chooses one producer from each pair and keeps both gates. Deleting reaction 0 leaves two alternatives; deleting either gate leaves none.

The algorithm selects one producer per nonfood species and one effective catalyst per reaction, prunes reactions that cannot form a RAF, and builds a graph from consumers to suppliers. Groups that are mutually reachable in this graph and have no outgoing dependencies (sink strongly connected components) are minimal in that resolved system. They still need an original-system check: restoring alternative catalysts can reveal a smaller RAF. The included counterexample explicitly generates and rejects such a candidate.

For six independent modules, the global loop inspects 64 supplier resolutions and the component loop only 12, producing the same 12 irrRAFs. For 64 independent modules, it inspects 128 rather than 2642^{64} combinations. Coupled gates remain exponential because their outputs themselves are exponential. A separate 64-reaction chain has no alternative suppliers, one resolution, and 64 food-generation stages.

The catalogue also supports practical structural queries. In the worked system, the cheapest destructive cut costs 5, while the weighted greedy approximation costs 6. With independent reaction availability 1/21/2, the exact probability that some RAF remains is 9/64. This measures static availability, not kinetic survival.

Download the package, install requirements.txt, run python example.py --output outputs, and check it with python -m unittest -v. Edit the food/reaction tuples and query inputs near the top of the source, or pass --input my_model.json. The README shows how to import the model and enumerator directly. A declared resolution budget prevents an unfinished search from being reported as a complete catalogue.

The checks compare both algorithms with an independent exhaustive oracle, verify every restriction of the worked system, and exercise the paper's startup and minimality counterexamples. These are exact finite calculations; the implementation is not extracted from Lean and does not compile the manuscript's formal proof. Original reaction identifiers, disjunctive catalysis, and inclusion-minimality are preserved throughout.

Python source

"""Complete structural irrRAF catalogues via deterministic supplier resolutions.

Catalysts do NOT gate food closure. Catalysis is disjunctive. This is a finite
structural calculation, not a kinetic or thermodynamic reactor simulation.
"""
from __future__ import annotations
import argparse
import csv
from dataclasses import dataclass, asdict
from fractions import Fraction
import hashlib
from itertools import combinations, product
import json
import math
from pathlib import Path
import platform
import time

# USER INPUTS ---------------------------------------------------------------
FOOD = ('f',)                     # species freely available to every reaction
# id, reactants, products, alternative catalysts; paper's k=2 gate example.
REACTIONS = (
    (0, ('f',), ('x1',), ('z2',)),
    (1, ('f',), ('x1',), ('z2',)),
    (2, ('f',), ('x2',), ('z2',)),
    (3, ('f',), ('x2',), ('z2',)),
    (4, ('x1',), ('z1',), ('z2',)),
    (5, ('z1', 'x2'), ('z2',), ('z2',)),
)
DELETION_COSTS = {0: 2, 1: 3, 2: 4, 3: 1, 4: 7, 5: 6}  # positive abstract costs
AVAILABLE_PROBABILITY = '1/2'     # independent static reaction availability
REQUIRED_REACTIONS = (0,)
FORBIDDEN_REACTIONS = (2,)
DELETED_REACTIONS = (0,)
SCALING_SIZES = (1, 2, 3, 4, 5, 6)  # paper families; controlled size sweep
LARGE_INDEPENDENT_MODULES = 64
DETERMINISTIC_CHAIN_LENGTH = 64
RESOLUTION_BUDGET = 4096          # fail explicitly before exceeding this budget
EXHAUSTIVE_REACTION_CAP = 18       # small-instance verification/queries only
PAPER_SHA256 = 'c92ad401d52719b68ab0118296ae5dc686c994cbaf9bea19a5bb8c4c4820523a'
# All inputs are finite identifiers/sets, counts, costs or probabilities;
# there are no concentration units or rate constants in the RAF predicate.
# --------------------------------------------------------------------------


@dataclass(frozen=True)
class Reaction:
    identifier: int
    reactants: frozenset[str]
    products: frozenset[str]
    catalysts: frozenset[str]

    def __post_init__(self):
        if not isinstance(self.identifier,int) or isinstance(self.identifier,bool):
            raise ValueError('Reaction identifiers must be integers.')
        for field in ('reactants','products','catalysts'):
            values=frozenset(getattr(self,field))
            if any(not isinstance(x,str) or not x for x in values):
                raise ValueError('Species names must be nonempty strings.')
            object.__setattr__(self,field,values)


class ReactionSystem:
    def __init__(self,food,reactions):
        self.food=frozenset(food)
        if any(not isinstance(x,str) or not x for x in self.food):
            raise ValueError('Food species names must be nonempty strings.')
        reactions=tuple(reactions)
        self.reactions={r.identifier:r for r in reactions}
        if len(self.reactions)!=len(reactions): raise ValueError('Reaction identifiers must be unique.')
        self.identifiers=frozenset(self.reactions)

    def selected(self,ids=None):
        chosen=self.identifiers if ids is None else frozenset(ids)
        if not chosen<=self.identifiers: raise ValueError('Unknown reaction identifier.')
        return chosen

    def closure_stages(self,ids=None):
        selected=self.selected(ids); stages=[self.food]
        while True:
            current=stages[-1]
            new=current.union(*(self.reactions[r].products for r in selected
                                if self.reactions[r].reactants<=current))
            if new==current: return stages
            stages.append(new)

    def closure(self,ids=None): return self.closure_stages(ids)[-1]

    def is_raf(self,ids):
        ids=self.selected(ids);closure=self.closure(ids)
        return bool(ids) and all(self.reactions[r].reactants<=closure
                                and self.reactions[r].catalysts&closure for r in ids)

    def maximal_raf(self,ids=None):
        current=self.selected(ids)
        while current:
            closure=self.closure(current)
            remaining=frozenset(r for r in current if self.reactions[r].reactants<=closure
                                and self.reactions[r].catalysts&closure)
            if remaining==current: return current
            current=remaining
        return frozenset()

    def restrict(self,ids):
        return ReactionSystem(self.food,[self.reactions[r] for r in sorted(self.selected(ids))])

    def record(self):
        return dict(food=sorted(self.food),reactions=[dict(id=r.identifier,reactants=sorted(r.reactants),
                products=sorted(r.products),catalysts=sorted(r.catalysts)) for r in self.reactions.values()])

    @classmethod
    def from_record(cls,data):
        return cls(data['food'],[Reaction(r['id'],r['reactants'],r['products'],r['catalysts']) for r in data['reactions']])


@dataclass
class SupplierOptions:
    maximal: frozenset[int]
    producers: dict[str,tuple[int,...]]
    catalysts: dict[int,tuple[str,...]]

    @classmethod
    def from_system(cls,system):
        maximal=system.maximal_raf()
        available=system.food.union(*(system.reactions[r].products for r in maximal))
        producers={x:tuple(sorted(r for r in maximal if x in system.reactions[r].products))
                   for x in sorted(available-system.food)}
        catalysts={}
        for r in sorted(maximal):
            food=system.reactions[r].catalysts&system.food
            catalysts[r]=(min(food),) if food else tuple(sorted(system.reactions[r].catalysts&available))
        return cls(maximal,producers,catalysts)

    @property
    def beta(self): return sum(len(v)-1 for v in (*self.producers.values(),*self.catalysts.values()))

    @property
    def count(self): return math.prod(len(v) for v in (*self.producers.values(),*self.catalysts.values()))

    def resolutions(self):
        keys=list(self.producers)+list(self.catalysts)
        options=list(self.producers.values())+list(self.catalysts.values())
        boundary=len(self.producers)
        for values in product(*options):
            yield dict(zip(keys[:boundary],values[:boundary])),dict(zip(keys[boundary:],values[boundary:]))


def resolve(system,options,producer,catalyst):
    return ReactionSystem(system.food,[Reaction(r,system.reactions[r].reactants,
        frozenset(x for x in system.reactions[r].products if x in system.food or producer[x]==r),
        frozenset((catalyst[r],))) for r in sorted(options.maximal)])


def sink_components(graph):
    """Iterative Kosaraju SCCs, avoiding recursion-depth limits on long chains."""
    visited=set();order=[]
    for root in sorted(graph):
        if root in visited: continue
        visited.add(root); stack=[(root,iter(sorted(graph[root])))]
        while stack:
            node,children=stack[-1]
            child=next(children,None)
            if child is None: order.append(node);stack.pop()
            elif child not in visited:
                visited.add(child);stack.append((child,iter(sorted(graph[child]))))
    reverse={v:set() for v in graph}
    for v,edges in graph.items():
        for w in edges: reverse[w].add(v)
    visited=set();components=[]
    for root in reversed(order):
        if root in visited: continue
        component=set();stack=[root];visited.add(root)
        while stack:
            node=stack.pop();component.add(node)
            for child in reverse[node]-visited:
                visited.add(child);stack.append(child)
        components.append(frozenset(component))
    return [c for c in components if all(graph[v]<=c for v in c)]


@dataclass
class Catalogue:
    members: tuple[frozenset[int],...]
    beta: int
    global_resolution_count: int
    resolutions_inspected: int
    candidates: int
    rejected: int
    validation_calls: int
    component_count: int
    largest_local_beta: int

    def filter(self,required=(),forbidden=()):
        required,forbidden=frozenset(required),frozenset(forbidden)
        if required&forbidden: raise ValueError('Required and forbidden identifiers overlap.')
        return tuple(c for c in self.members if required<=c and not forbidden&c)

    def frequencies(self):
        return {r:sum(r in c for c in self.members) for r in sorted(set().union(*self.members))}


class SupplierEnumerator:
    def __init__(self,resolution_budget=RESOLUTION_BUDGET):
        if not isinstance(resolution_budget,int) or resolution_budget<1:
            raise ValueError('Resolution budget must be a positive integer.')
        self.budget=resolution_budget

    def global_catalogue(self,system):
        options=SupplierOptions.from_system(system)
        if options.count>self.budget:
            raise ValueError(f'{options.count} resolutions exceed budget {self.budget}; use components or raise budget explicitly.')
        members=set();candidates=rejected=calls=inspected=0
        if options.maximal:
            for producer,catalyst in options.resolutions():
                inspected+=1
                resolved=resolve(system,options,producer,catalyst)
                kept=resolved.maximal_raf()
                graph={r:{producer[x] for x in (resolved.reactions[r].reactants|resolved.reactions[r].catalysts)-system.food}
                       for r in kept}
                if any(not edges<=kept for edges in graph.values()): raise ArithmeticError('Dependency escaped pruned system.')
                resolution_calls=0
                for component in sink_components(graph):
                    candidates+=1;valid=True
                    for r in sorted(component):
                        calls+=1;resolution_calls+=1
                        if system.maximal_raf(component-{r}): valid=False;break
                    if valid: members.add(component)
                    else: rejected+=1
                if resolution_calls>len(options.maximal): raise ArithmeticError('Validation budget invariant failed.')
        ordered=tuple(sorted(members,key=lambda c:tuple(sorted(c))))
        result=Catalogue(ordered,options.beta,options.count,inspected,candidates,rejected,calls,
                         1 if options.maximal else 0,options.beta)
        if sum(map(len,ordered))>len(options.maximal)*options.count: raise ArithmeticError('Output membership bound failed.')
        return result

    def components(self,system):
        options=SupplierOptions.from_system(system)
        # Every nonfood reactant, product and effective catalyst incidence counts.
        touching={}
        for r in options.maximal:
            reaction=system.reactions[r]
            for x in (reaction.reactants|reaction.products|frozenset(options.catalysts[r]))-system.food:
                touching.setdefault(x,set()).add(r)
        adjacency={r:set() for r in options.maximal}
        for group in touching.values():
            first=min(group)
            for r in group: adjacency[first].add(r);adjacency[r].add(first)
        blocks=[];seen=set()
        for r in sorted(adjacency):
            if r in seen: continue
            block=set();stack=[r];seen.add(r)
            while stack:
                v=stack.pop();block.add(v)
                for w in adjacency[v]-seen: seen.add(w);stack.append(w)
            blocks.append(frozenset(block))
        return blocks

    def catalogue(self,system):
        options=SupplierOptions.from_system(system);blocks=self.components(system)
        local=[system.restrict(b) for b in blocks]
        required=sum(SupplierOptions.from_system(q).count for q in local)
        if required>self.budget: raise ValueError(f'Total local resolutions {required} exceed budget {self.budget}.')
        results=[self.global_catalogue(q) for q in local]
        return Catalogue(tuple(sorted((c for result in results for c in result.members),key=lambda c:tuple(sorted(c)))),
                         options.beta,options.count,sum(r.resolutions_inspected for r in results),
                         sum(r.candidates for r in results),sum(r.rejected for r in results),
                         sum(r.validation_calls for r in results),len(blocks),max((r.beta for r in results),default=0))


def coupled_gates(k):
    if not isinstance(k,int) or k<1: raise ValueError('Gate count must be a positive integer.')
    reactions=[]
    for i in range(k):
        for alternative in range(2):
            reactions.append(Reaction(2*i+alternative,{'f'},{f'x{i+1}'},{f'z{k}'}))
    for i in range(k):
        reactants={f'x{i+1}'}|({f'z{i}'} if i else set())
        reactions.append(Reaction(2*k+i,reactants,{f'z{i+1}'},{f'z{k}'}))
    return ReactionSystem({'f'},reactions)


def independent_modules(count,choices=True):
    if not isinstance(count,int) or count<1: raise ValueError('Module count must be positive integer.')
    reactions=[];per=3 if choices else 2
    for i in range(count):
        for a in range(per-1): reactions.append(Reaction(per*i+a,{'f'},{f'x{i}'},{f'z{i}'}))
        reactions.append(Reaction(per*i+per-1,{f'x{i}'},{f'z{i}'},{f'z{i}'}))
    return ReactionSystem({'f'},reactions)


def deterministic_chain(length):
    if not isinstance(length,int) or length<1: raise ValueError('Chain length must be positive integer.')
    return ReactionSystem({'f'},[Reaction(i,{'f' if i==0 else f'x{i}'},{f'x{i+1}'},{f'x{length}'}) for i in range(length)])


def minimal_cuts(system,catalogue):
    if len(system.identifiers)>EXHAUSTIVE_REACTION_CAP: raise ValueError('Exact cut search exceeds subset budget.')
    found=[]
    for size in range(len(system.identifiers)+1):
        for values in combinations(sorted(system.identifiers),size):
            cut=frozenset(values)
            if not any(c<=cut for c in found) and all(cut&member for member in catalogue.members): found.append(cut)
    return tuple(found)


def greedy_cut(catalogue,costs):
    costs={r:Fraction(str(c)) for r,c in costs.items()}
    if any(c<0 for c in costs.values()): raise ValueError('Deletion costs must be nonnegative.')
    remaining=set(catalogue.members);chosen=[]
    while remaining:
        options=[(cost/sum(r in c for c in remaining),r) for r,cost in costs.items() if any(r in c for c in remaining)]
        if not options: raise ValueError('Eligible reactions cannot hit all irrRAFs.')
        _,r=min(options);chosen.append(r);remaining={c for c in remaining if r not in c}
    return chosen,sum((costs[r] for r in chosen),Fraction(0))


def exact_availability(system,catalogue,p=Fraction(AVAILABLE_PROBABILITY)):
    p=Fraction(p)
    if not 0<=p<=1: raise ValueError('Availability must lie in [0,1].')
    n=len(system.identifiers)
    if n>EXHAUSTIVE_REACTION_CAP: raise ValueError('Exact availability search exceeds subset budget.')
    total=Fraction(0)
    for size in range(n+1):
        for ids in combinations(sorted(system.identifiers),size):
            if any(c<=frozenset(ids) for c in catalogue.members): total+=p**size*(1-p)**(n-size)
    return total


def zero_excess_reliability(catalogue,p=Fraction(AVAILABLE_PROBABILITY)):
    p=Fraction(p)
    if catalogue.beta!=0: raise ValueError('Disjoint zero-excess formula does not apply to this catalogue.')
    if not 0<=p<=1: raise ValueError('Availability must lie in [0,1].')
    return 1-math.prod(1-p**len(c) for c in catalogue.members)


def family_record(result):
    return dict(members=[sorted(c) for c in result.members],beta=result.beta,
                global_resolution_count=result.global_resolution_count,resolutions_inspected=result.resolutions_inspected,
                candidates=result.candidates,rejected=result.rejected,validation_calls=result.validation_calls,
                component_count=result.component_count,largest_local_beta=result.largest_local_beta,
                frequencies=result.frequencies(),total_memberships=sum(map(len,result.members)))


def figures(system,catalogue,scaling,output):
    import matplotlib
    matplotlib.use('Agg')
    import matplotlib.pyplot as plt
    with plt.rc_context({'font.size':11,'axes.spines.top':False,'axes.spines.right':False,
                         'figure.facecolor':'white','savefig.facecolor':'white','svg.fonttype':'none'}):
        if catalogue.members and len(catalogue.members)<=32 and len(system.identifiers)<=30:
            fig,ax=plt.subplots(figsize=(7.2,4.5),layout='constrained')
            ids=sorted(system.identifiers)
            ax.imshow([[int(r in c) for r in ids] for c in catalogue.members],cmap='Blues',vmin=0,vmax=1,aspect='auto',interpolation='nearest')
            ax.set(xticks=range(len(ids)),xticklabels=ids,yticks=range(len(catalogue.members)),
                   yticklabels=[f'I{i+1}' for i in range(len(catalogue.members))],
                   xlabel='Original reaction identifier',ylabel='Irreducible RAF')
            for i,c in enumerate(catalogue.members):
                for j,r in enumerate(ids):
                    ax.text(j,i,'1' if r in c else '0',ha='center',va='center',color='white' if r in c else '#202124')
            fig.savefig(output/'catalogue.png',dpi=220);fig.savefig(output/'catalogue.svg');plt.close(fig)
        fig,ax=plt.subplots(figsize=(7.2,4.5),layout='constrained')
        for family,mode,color,style in [('independent','global','#D55E00','--'),('independent','components','#0072B2','-'),('coupled','components','#7B3294',':')]:
            rows=[r for r in scaling if r['family']==family and r['mode']==mode]
            ax.plot([r['size'] for r in rows],[r['resolutions_inspected'] for r in rows],marker='o',linestyle=style,color=color,label=f'{family.capitalize()}, {mode}')
        ax.set(xlabel='Number of modules or gates',ylabel='Supplier resolutions inspected',yscale='log',xticks=list(SCALING_SIZES))
        ax.legend(fontsize=9);ax.grid(axis='y',color='#e3e6e8')
        fig.savefig(output/'scaling.png',dpi=220);fig.savefig(output/'scaling.svg');plt.close(fig)


def main():
    parser=argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--output',type=Path,default=Path('outputs'))
    parser.add_argument('--input',type=Path,help='Optional JSON food/reactions model; see model.json.')
    args=parser.parse_args();args.output.mkdir(parents=True,exist_ok=True);start=time.perf_counter()
    system=ReactionSystem.from_record(json.loads(args.input.read_text())) if args.input else ReactionSystem(FOOD,[Reaction(*r) for r in REACTIONS])
    inputs=dict(model=system.record(),resolution_budget=RESOLUTION_BUDGET,availability=AVAILABLE_PROBABILITY,
                deletion_costs=DELETION_COSTS,required=REQUIRED_REACTIONS,forbidden=FORBIDDEN_REACTIONS,deleted=DELETED_REACTIONS,
                scaling_sizes=SCALING_SIZES,large_modules=LARGE_INDEPENDENT_MODULES,chain_length=DETERMINISTIC_CHAIN_LENGTH)
    intro='Resolved inputs: '+json.dumps(inputs);print(intro,flush=True)
    engine=SupplierEnumerator();catalogue=engine.catalogue(system)
    summary=dict(inputs=inputs,catalogue=family_record(catalogue))
    if not args.input:
        cuts=minimal_cuts(system,catalogue);chosen,cost=greedy_cut(catalogue,DELETION_COSTS)
        summary['worked_queries']=dict(required=list(REQUIRED_REACTIONS),forbidden=list(FORBIDDEN_REACTIONS),
            matching=[sorted(c) for c in catalogue.filter(REQUIRED_REACTIONS,FORBIDDEN_REACTIONS)],
            after_deletion=[sorted(c) for c in catalogue.filter(forbidden=DELETED_REACTIONS)],
            minimal_cuts=[sorted(c) for c in cuts],minimum_cut_cost=min(sum(DELETION_COSTS[r] for r in c) for c in cuts),
            greedy_cut=chosen,greedy_cost=str(cost),availability_probability=str(exact_availability(system,catalogue)))
    # Paper's catalyst-alternative example: resolved minimum is not original minimum.
    counterexample=ReactionSystem({'f'},[Reaction(0,{'f'},{'x'},{'x','y'}),Reaction(1,{'x'},{'y'},{'x'})])
    summary['necessary_validation']=family_record(engine.global_catalogue(counterexample))
    scaling=[]
    for size in SCALING_SIZES:
        for family,builder in [('independent',independent_modules),('coupled',coupled_gates)]:
            model=builder(size)
            for mode,enumerate_ in [('global',engine.global_catalogue),('components',engine.catalogue)]:
                result=enumerate_(model)
                scaling.append(dict(family=family,mode=mode,size=size,reactions=len(model.identifiers),
                    beta=result.beta,global_resolution_count=result.global_resolution_count,
                    resolutions_inspected=result.resolutions_inspected,outputs=len(result.members),
                    validation_calls=result.validation_calls,total_memberships=sum(map(len,result.members))))
    large=engine.catalogue(independent_modules(LARGE_INDEPENDENT_MODULES))
    summary['large_independent']=family_record(large)
    chain=deterministic_chain(DETERMINISTIC_CHAIN_LENGTH)
    summary['deterministic_chain']=dict(**family_record(engine.global_catalogue(chain)),closure_depth=len(chain.closure_stages())-1)
    zero=engine.catalogue(independent_modules(4,choices=False))
    summary['zero_excess_availability']=str(zero_excess_reliability(zero))
    summary['evidence']='Exact finite set computations and rational availability probabilities. Bounded independent tests, not Lean extraction or compilation. Structural RAF membership does not assert kinetic function.'
    with (args.output/'scaling.csv').open('w',newline='',encoding='utf-8') as f:
        writer=csv.DictWriter(f,fieldnames=list(scaling[0]));writer.writeheader();writer.writerows(scaling)
    with (args.output/'catalogue.csv').open('w',newline='',encoding='utf-8') as f:
        writer=csv.writer(f);writer.writerow(['irrRAF','reaction_ids']);writer.writerows((i+1,' '.join(map(str,sorted(c)))) for i,c in enumerate(catalogue.members))
    (args.output/'model.json').write_text(json.dumps(system.record(),indent=2)+'\n',encoding='utf-8')
    figures(system,catalogue,scaling,args.output)
    transcript=json.dumps(summary,indent=2)
    (args.output/'summary.json').write_text(transcript+'\n',encoding='utf-8')
    (args.output/'console.txt').write_text(intro+'\n'+transcript+'\n',encoding='utf-8')
    import matplotlib
    metadata=dict(paper_sha256=PAPER_SHA256,source_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
        matplotlib=matplotlib.__version__,
        python=platform.python_version(),platform=platform.platform(),processor=platform.processor(),
        elapsed_seconds=time.perf_counter()-start,command='python example.py --output outputs',
        seed_policy='Deterministic enumeration; no stochastic sampling.',
        output_sha256={p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(args.output.iterdir()) if p.is_file() and p.name!='run_metadata.json'})
    (args.output/'run_metadata.json').write_text(json.dumps(metadata,indent=2)+'\n',encoding='utf-8')
    print(transcript)


if __name__=='__main__': main()
Run output
Resolved inputs: {"model": {"food": ["f"], "reactions": [{"id": 0, "reactants": ["f"], "products": ["x1"], "catalysts": ["z2"]}, {"id": 1, "reactants": ["f"], "products": ["x1"], "catalysts": ["z2"]}, {"id": 2, "reactants": ["f"], "products": ["x2"], "catalysts": ["z2"]}, {"id": 3, "reactants": ["f"], "products": ["x2"], "catalysts": ["z2"]}, {"id": 4, "reactants": ["x1"], "products": ["z1"], "catalysts": ["z2"]}, {"id": 5, "reactants": ["x2", "z1"], "products": ["z2"], "catalysts": ["z2"]}]}, "resolution_budget": 4096, "availability": "1/2", "deletion_costs": {"0": 2, "1": 3, "2": 4, "3": 1, "4": 7, "5": 6}, "required": [0], "forbidden": [2], "deleted": [0], "scaling_sizes": [1, 2, 3, 4, 5, 6], "large_modules": 64, "chain_length": 64}
{
  "inputs": {
    "model": {
      "food": [
        "f"
      ],
      "reactions": [
        {
          "id": 0,
          "reactants": [
            "f"
          ],
          "products": [
            "x1"
          ],
          "catalysts": [
            "z2"
          ]
        },
        {
          "id": 1,
          "reactants": [
            "f"
          ],
          "products": [
            "x1"
          ],
          "catalysts": [
            "z2"
          ]
        },
        {
          "id": 2,
          "reactants": [
            "f"
          ],
          "products": [
            "x2"
          ],
          "catalysts": [
            "z2"
          ]
        },
        {
          "id": 3,
          "reactants": [
            "f"
          ],
          "products": [
            "x2"
          ],
          "catalysts": [
            "z2"
          ]
        },
        {
          "id": 4,
          "reactants": [
            "x1"
          ],
          "products": [
            "z1"
          ],
          "catalysts": [
            "z2"
          ]
        },
        {
          "id": 5,
          "reactants": [
            "x2",
            "z1"
          ],
          "products": [
            "z2"
          ],
          "catalysts": [
            "z2"
          ]
        }
      ]
    },
    "resolution_budget": 4096,
    "availability": "1/2",
    "deletion_costs": {
      "0": 2,
      "1": 3,
      "2": 4,
      "3": 1,
      "4": 7,
      "5": 6
    },
    "required": [
      0
    ],
    "forbidden": [
      2
    ],
    "deleted": [
      0
    ],
    "scaling_sizes": [
      1,
      2,
      3,
      4,
      5,
      6
    ],
    "large_modules": 64,
    "chain_length": 64
  },
  "catalogue": {
    "members": [
      [
        0,
        2,
        4,
        5
      ],
      [
        0,
        3,
        4,
        5
      ],
      [
        1,
        2,
        4,
        5
      ],
      [
        1,
        3,
        4,
        5
      ]
    ],
    "beta": 2,
    "global_resolution_count": 4,
    "resolutions_inspected": 4,
    "candidates": 4,
    "rejected": 0,
    "validation_calls": 16,
    "component_count": 1,
    "largest_local_beta": 2,
    "frequencies": {
      "0": 2,
      "1": 2,
      "2": 2,
      "3": 2,
      "4": 4,
      "5": 4
    },
    "total_memberships": 16
  },
  "worked_queries": {
    "required": [
      0
    ],
    "forbidden": [
      2
    ],
    "matching": [
      [
        0,
        3,
        4,
        5
      ]
    ],
    "after_deletion": [
      [
        1,
        2,
        4,
        5
      ],
      [
        1,
        3,
        4,
        5
      ]
    ],
    "minimal_cuts": [
      [
        4
      ],
      [
        5
      ],
      [
        0,
        1
      ],
      [
        2,
        3
      ]
    ],
    "minimum_cut_cost": 5,
    "greedy_cut": [
      3,
      0,
      1
    ],
    "greedy_cost": "6",
    "availability_probability": "9/64"
  },
  "necessary_validation": {
    "members": [
      [
        0
      ]
    ],
    "beta": 1,
    "global_resolution_count": 2,
    "resolutions_inspected": 2,
    "candidates": 2,
    "rejected": 1,
    "validation_calls": 3,
    "component_count": 1,
    "largest_local_beta": 1,
    "frequencies": {
      "0": 1
    },
    "total_memberships": 1
  },
  "large_independent": {
    "members": [
      [
        0,
        2
      ],
      [
        1,
        2
      ],
      [
        3,
        5
      ],
      [
        4,
        5
      ],
      [
        6,
        8
      ],
      [
        7,
        8
      ],
      [
        9,
        11
      ],
      [
        10,
        11
      ],
      [
        12,
        14
      ],
      [
        13,
        14
      ],
      [
        15,
        17
      ],
      [
        16,
        17
      ],
      [
        18,
        20
      ],
      [
        19,
        20
      ],
      [
        21,
        23
      ],
      [
        22,
        23
      ],
      [
        24,
        26
      ],
      [
        25,
        26
      ],
      [
        27,
        29
      ],
      [
        28,
        29
      ],
      [
        30,
        32
      ],
      [
        31,
        32
      ],
      [
        33,
        35
      ],
      [
        34,
        35
      ],
      [
        36,
        38
      ],
      [
        37,
        38
      ],
      [
        39,
        41
      ],
      [
        40,
        41
      ],
      [
        42,
        44
      ],
      [
        43,
        44
      ],
      [
        45,
        47
      ],
      [
        46,
        47
      ],
      [
        48,
        50
      ],
      [
        49,
        50
      ],
      [
        51,
        53
      ],
      [
        52,
        53
      ],
      [
        54,
        56
      ],
      [
        55,
        56
      ],
      [
        57,
        59
      ],
      [
        58,
        59
      ],
      [
        60,
        62
      ],
      [
        61,
        62
      ],
      [
        63,
        65
      ],
      [
        64,
        65
      ],
      [
        66,
        68
      ],
      [
        67,
        68
      ],
      [
        69,
        71
      ],
      [
        70,
        71
      ],
      [
        72,
        74
      ],
      [
        73,
        74
      ],
      [
        75,
        77
      ],
      [
        76,
        77
      ],
      [
        78,
        80
      ],
      [
        79,
        80
      ],
      [
        81,
        83
      ],
      [
        82,
        83
      ],
      [
        84,
        86
      ],
      [
        85,
        86
      ],
      [
        87,
        89
      ],
      [
        88,
        89
      ],
      [
        90,
        92
      ],
      [
        91,
        92
      ],
      [
        93,
        95
      ],
      [
        94,
        95
      ],
      [
        96,
        98
      ],
      [
        97,
        98
      ],
      [
        99,
        101
      ],
      [
        100,
        101
      ],
      [
        102,
        104
      ],
      [
        103,
        104
      ],
      [
        105,
        107
      ],
      [
        106,
        107
      ],
      [
        108,
        110
      ],
      [
        109,
        110
      ],
      [
        111,
        113
      ],
      [
        112,
        113
      ],
      [
        114,
        116
      ],
      [
        115,
        116
      ],
      [
        117,
        119
      ],
      [
        118,
        119
      ],
      [
        120,
        122
      ],
      [
        121,
        122
      ],
      [
        123,
        125
      ],
      [
        124,
        125
      ],
      [
        126,
        128
      ],
      [
        127,
        128
      ],
      [
        129,
        131
      ],
      [
        130,
        131
      ],
      [
        132,
        134
      ],
      [
        133,
        134
      ],
      [
        135,
        137
      ],
      [
        136,
        137
      ],
      [
        138,
        140
      ],
      [
        139,
        140
      ],
      [
        141,
        143
      ],
      [
        142,
        143
      ],
      [
        144,
        146
      ],
      [
        145,
        146
      ],
      [
        147,
        149
      ],
      [
        148,
        149
      ],
      [
        150,
        152
      ],
      [
        151,
        152
      ],
      [
        153,
        155
      ],
      [
        154,
        155
      ],
      [
        156,
        158
      ],
      [
        157,
        158
      ],
      [
        159,
        161
      ],
      [
        160,
        161
      ],
      [
        162,
        164
      ],
      [
        163,
        164
      ],
      [
        165,
        167
      ],
      [
        166,
        167
      ],
      [
        168,
        170
      ],
      [
        169,
        170
      ],
      [
        171,
        173
      ],
      [
        172,
        173
      ],
      [
        174,
        176
      ],
      [
        175,
        176
      ],
      [
        177,
        179
      ],
      [
        178,
        179
      ],
      [
        180,
        182
      ],
      [
        181,
        182
      ],
      [
        183,
        185
      ],
      [
        184,
        185
      ],
      [
        186,
        188
      ],
      [
        187,
        188
      ],
      [
        189,
        191
      ],
      [
        190,
        191
      ]
    ],
    "beta": 64,
    "global_resolution_count": 18446744073709551616,
    "resolutions_inspected": 128,
    "candidates": 128,
    "rejected": 0,
    "validation_calls": 256,
    "component_count": 64,
    "largest_local_beta": 1,
    "frequencies": {
      "0": 1,
      "1": 1,
      "2": 2,
      "3": 1,
      "4": 1,
      "5": 2,
      "6": 1,
      "7": 1,
      "8": 2,
      "9": 1,
      "10": 1,
      "11": 2,
      "12": 1,
      "13": 1,
      "14": 2,
      "15": 1,
      "16": 1,
      "17": 2,
      "18": 1,
      "19": 1,
      "20": 2,
      "21": 1,
      "22": 1,
      "23": 2,
      "24": 1,
      "25": 1,
      "26": 2,
      "27": 1,
      "28": 1,
      "29": 2,
      "30": 1,
      "31": 1,
      "32": 2,
      "33": 1,
      "34": 1,
      "35": 2,
      "36": 1,
      "37": 1,
      "38": 2,
      "39": 1,
      "40": 1,
      "41": 2,
      "42": 1,
      "43": 1,
      "44": 2,
      "45": 1,
      "46": 1,
      "47": 2,
      "48": 1,
      "49": 1,
      "50": 2,
      "51": 1,
      "52": 1,
      "53": 2,
      "54": 1,
      "55": 1,
      "56": 2,
      "57": 1,
      "58": 1,
      "59": 2,
      "60": 1,
      "61": 1,
      "62": 2,
      "63": 1,
      "64": 1,
      "65": 2,
      "66": 1,
      "67": 1,
      "68": 2,
      "69": 1,
      "70": 1,
      "71": 2,
      "72": 1,
      "73": 1,
      "74": 2,
      "75": 1,
      "76": 1,
      "77": 2,
      "78": 1,
      "79": 1,
      "80": 2,
      "81": 1,
      "82": 1,
      "83": 2,
      "84": 1,
      "85": 1,
      "86": 2,
      "87": 1,
      "88": 1,
      "89": 2,
      "90": 1,
      "91": 1,
      "92": 2,
      "93": 1,
      "94": 1,
      "95": 2,
      "96": 1,
      "97": 1,
      "98": 2,
      "99": 1,
      "100": 1,
      "101": 2,
      "102": 1,
      "103": 1,
      "104": 2,
      "105": 1,
      "106": 1,
      "107": 2,
      "108": 1,
      "109": 1,
      "110": 2,
      "111": 1,
      "112": 1,
      "113": 2,
      "114": 1,
      "115": 1,
      "116": 2,
      "117": 1,
      "118": 1,
      "119": 2,
      "120": 1,
      "121": 1,
      "122": 2,
      "123": 1,
      "124": 1,
      "125": 2,
      "126": 1,
      "127": 1,
      "128": 2,
      "129": 1,
      "130": 1,
      "131": 2,
      "132": 1,
      "133": 1,
      "134": 2,
      "135": 1,
      "136": 1,
      "137": 2,
      "138": 1,
      "139": 1,
      "140": 2,
      "141": 1,
      "142": 1,
      "143": 2,
      "144": 1,
      "145": 1,
      "146": 2,
      "147": 1,
      "148": 1,
      "149": 2,
      "150": 1,
      "151": 1,
      "152": 2,
      "153": 1,
      "154": 1,
      "155": 2,
      "156": 1,
      "157": 1,
      "158": 2,
      "159": 1,
      "160": 1,
      "161": 2,
      "162": 1,
      "163": 1,
      "164": 2,
      "165": 1,
      "166": 1,
      "167": 2,
      "168": 1,
      "169": 1,
      "170": 2,
      "171": 1,
      "172": 1,
      "173": 2,
      "174": 1,
      "175": 1,
      "176": 2,
      "177": 1,
      "178": 1,
      "179": 2,
      "180": 1,
      "181": 1,
      "182": 2,
      "183": 1,
      "184": 1,
      "185": 2,
      "186": 1,
      "187": 1,
      "188": 2,
      "189": 1,
      "190": 1,
      "191": 2
    },
    "total_memberships": 256
  },
  "deterministic_chain": {
    "members": [
      [
        0,
        1,
        2,
        3,
        4,
        5,
        6,
        7,
        8,
        9,
        10,
        11,
        12,
        13,
        14,
        15,
        16,
        17,
        18,
        19,
        20,
        21,
        22,
        23,
        24,
        25,
        26,
        27,
        28,
        29,
        30,
        31,
        32,
        33,
        34,
        35,
        36,
        37,
        38,
        39,
        40,
        41,
        42,
        43,
        44,
        45,
        46,
        47,
        48,
        49,
        50,
        51,
        52,
        53,
        54,
        55,
        56,
        57,
        58,
        59,
        60,
        61,
        62,
        63
      ]
    ],
    "beta": 0,
    "global_resolution_count": 1,
    "resolutions_inspected": 1,
    "candidates": 1,
    "rejected": 0,
    "validation_calls": 64,
    "component_count": 1,
    "largest_local_beta": 0,
    "frequencies": {
      "0": 1,
      "1": 1,
      "2": 1,
      "3": 1,
      "4": 1,
      "5": 1,
      "6": 1,
      "7": 1,
      "8": 1,
      "9": 1,
      "10": 1,
      "11": 1,
      "12": 1,
      "13": 1,
      "14": 1,
      "15": 1,
      "16": 1,
      "17": 1,
      "18": 1,
      "19": 1,
      "20": 1,
      "21": 1,
      "22": 1,
      "23": 1,
      "24": 1,
      "25": 1,
      "26": 1,
      "27": 1,
      "28": 1,
      "29": 1,
      "30": 1,
      "31": 1,
      "32": 1,
      "33": 1,
      "34": 1,
      "35": 1,
      "36": 1,
      "37": 1,
      "38": 1,
      "39": 1,
      "40": 1,
      "41": 1,
      "42": 1,
      "43": 1,
      "44": 1,
      "45": 1,
      "46": 1,
      "47": 1,
      "48": 1,
      "49": 1,
      "50": 1,
      "51": 1,
      "52": 1,
      "53": 1,
      "54": 1,
      "55": 1,
      "56": 1,
      "57": 1,
      "58": 1,
      "59": 1,
      "60": 1,
      "61": 1,
      "62": 1,
      "63": 1
    },
    "total_memberships": 64,
    "closure_depth": 64
  },
  "zero_excess_availability": "175/256",
  "evidence": "Exact finite set computations and rational availability probabilities. Bounded independent tests, not Lean extraction or compilation. Structural RAF membership does not assert kinetic function."
}