The Mathematics of Brazilian Jiu-Jitsu

I was a Brazilian Jiu Jitsu practioner in past days and wrote this after thinking about the mathematical properties of this kinetic art form.

Ten places where real math earns its keep on the mat. Absorbing Markov chains, a policy solver, statics, allometry, survival curves, zero-sum games, tangles, plus the code that produced every number below.

1. The position graph

Start with the only object you need. Brazilian jiu-jitsu is a set of positions and a set of ways to get between them, which is a directed graph. Add the fact that two of those positions are terminal, the ones where somebody taps, and you have an absorbing Markov chain, which is a structure with a lot of machinery attached to it.

The trick that makes it work is writing every state from one player’s point of view. Not “mount” but mount and mounted, as separate states. Do that and the two absorbing states become “I finish” and “I get finished,” and the whole thing becomes answerable.

Sixteen live states, two absorbing. Here they are, arranged so that height is the answer the chain gives back:

The position graph. Height is the probability that a position eventually produces the finish for you rather than against you. Blue arrows gain ground, rust arrows lose it; arrow weight is transition probability, and only transitions above 7% are drawn or the middle becomes unreadable.

The figure is symmetric about the halfway line, and the symmetry is forced. Relabelling which player is which is an involution on the state space: mount_top swaps with mount_bot, WIN swaps with LOSS, and standing swaps with itself. The chain commutes with that swap. So if p(x) is the probability of finishing from state x:

p(x) + p(x̄) = 1 for every state, so p = ½ at every symmetric position.

This is the single most useful thing in the whole article, because it is a free unit test. You do not have to specify the bottom half of the matrix at all. Generate it from the involution. And when you compute absorption probabilities, standing and 50/50 must come out to exactly one half. Mine come out to 0.500 with a residual of 2 × 10^−16. If yours do not, your transition counts are inconsistent and you have a bug, not a discovery.

Absorption probabilities come from the fundamental matrix. Split the transition matrix into the transient block Q and the absorbing block R, then:

# P is (n+2) x (n+2); first n rows/cols are the live positions
Q, R = P[:n, :n], P[:n, n:]
N = np.linalg.inv(np.eye(n) - Q)   # fundamental matrix
B = N @ R                          # P(each finish | each start)
t = N.sum(axis=1)                  # expected events to absorption

Four lines. Everything in the next three sections is downstream of them.

The positional hierarchy, derived rather than asserted. Bars run from the halfway line. The two symmetric positions have no bar at all, because they are exactly even.

The order is roughly what any gym teaches, which is reassuring but not interesting. Back control on top, then mount, then side control. What’s interesting is where the model disagrees with the way people talk.

Closed guard from the bottom comes out at 0.558, a winning position, better than passing someone’s open guard (0.529), and well ahead of the man kneeling inside it (0.442). The chain isn’t being sentimental about guard players. It’s counting: closed guard bottom routes into the two best attacking positions in the sport more often than it routes into being passed, and the top player’s most likely outcomes are stalling or getting swept. Whether you believe 0.558 depends entirely on whether you believe my transition weights, which brings us to the disclaimer.

These numbers are priors, not data. Every weight in that matrix is hand-set from experience. I have not tagged ten thousand matches. The structure is real and the method is real; the specific decimals are an informed guess, and I have deliberately not written them to three significant figures anywhere it matters. What makes this worth publishing anyway is that the weights are the only input. Replace them with counts off your own footage and every figure in this article recomputes. Section 10 works out how much footage you would need before the recomputed numbers mean anything.
The chain has no clock. There are exactly two absorbing states, so in this model every match ends in a submission. That’s an ADCC-overtime idealization, not an IBJJF points match, and it’s why I phrase the vertical axis as “eventually produces the finish” rather than “wins.” Adding a third absorbing state for time expiring is a one-line change, and it’s the right change if you’re modelling a points ruleset. It also destroys the clean symmetry result above, because you then have to decide who wins on points, and that decision is where all the interesting refereeing arguments live.
No single ruleset is assumed, and that is a choice. The pieces here do not all come from one competition format. Section 6 uses IBJJF’s gi weight classes. Section 8’s collar sleeve is a gi-only guard. Section 9’s entanglements are built around heel hooks, which IBJJF bans in the gi outright. The chain itself is submission-only. That is deliberate: the methods are what is on offer and every one of them is ruleset-agnostic, so pinning the article to a single format would narrow it for no analytical gain. But it costs something specific. With no points, positional value counts submissions and nothing else. Under IBJJF scoring, passing the guard is worth three and mount is worth four whether or not you finish, which would lift the top of the hierarchy and make closed guard bottom look a good deal worse than the 0.558 it gets here. If you do commit to a format, that is the first thing to fix.

2. Time, and the fundamental matrix

The same matrix N that gave you win probabilities also gives you expected duration for free. Row sums of N are the expected number of positional events before somebody taps. That is a different question from who wins, and it separates positions that the hierarchy lumps together.

Expected number of positional events before somebody taps. Mirror positions share a row value, because it is the same match seen from two sides.

Notice the column is symmetric: back control and back taken both sit at 4 events. Of course they do. It is the same match seen from two sides, and it ends when it ends. Duration is a property of the position, not of who is winning it.

The useful reading is the spread. Back control resolves in about 4 events; standing takes about 8. Leg entanglements resolve faster (6.3) than standing does despite both being dead even, which is the quantitative version of something every leg locker will tell you: 50/50 is a coin flip, but it is a fast coin flip. Even positions are not all equally patient.

3. Value is not traffic

Positional value says what a position is worth once you are there. It says nothing about how much of the match passes through it. Those are different questions and they have different answers, which is the sort of thing a graph makes obvious and prose does not.

Value against eigenvector centrality. Mirror-image positions sit at exactly the same centrality, another consequence of the involution, since the undirected graph can’t tell the players apart. The vertical pairs are the same position seen from both sides.

Mount is the extreme case. It is the second most valuable position in the sport and the least central thing on the board. Very little routes through mount; you arrive there, and then the match is more or less over. It is a destination.

The turtle is the opposite: highest centrality of any position, and only middling value. Everything flows through it. Failed takedowns land there, failed guard pulls land there, people scramble through it in both directions. It is the busiest intersection in jiu-jitsu and almost nobody drills it as a position in its own right.

That gap is the actionable finding, and it is an argument about how to spend training time rather than an argument about which position is best. Value tells you what to chase. Centrality tells you what you will actually be in. If you want to know where marginal hours go, the answer is the high-centrality positions, because that is where the mass of the distribution sits. The gym’s positional hierarchy, being a ranking by value, is silent on the question.

4. What a solver decides

So far the chain has been passive: it describes what happens. Add choices and it becomes a Markov decision process, which can be asked what you should do.

I did not want to invent a separate transition table per technique, since that would be fifty more hand-set numbers and fifty more chances to smuggle in my own opinions. Instead an action reweights the transitions already in the chain, by outcome class. Advance multiplies up the moves to better positions and multiplies down finishes. Attack buys finish probability and pays for it in positional risk, in both directions. Stall inflates the self-loop. Disengage routes back to standing.

Reward is +1 for a submission, −1 for being submitted, and a discount factor γ applied per positional event. That γ is the whole story, because here it functions as the match clock. High γ means a finish twenty exchanges from now is nearly as good as a finish right now, which is what an open mat feels like. Low γ means it is very late in a match you are losing.

r = np.zeros(n_states); r[WIN], r[LOSS] = 1.0, -1.0
V = r.copy()
while True:                                  # value iteration
    Q = np.stack([Pa[a][:n] @ (r + gamma * V) for a in ACTIONS])
    new = V.copy(); new[:n] = Q.max(axis=0)
    if np.abs(new - V).max() < 1e-12: break
    V = new
policy = {s: ACTIONS[Q[:, i].argmax()] for i, s in enumerate(states)}
Optimal action in every position, as a function of how much the clock is pressing. Rows are ordered by positional value. Nothing about “position before submission” was put into the model; it comes out of the discount factor.

Read the figure from the right. With a patient clock, the solver attacks from exactly two positions, back control and mount, and advances from everywhere else. That is “position before submission,” and I want to be clear that it was not encoded anywhere. It falls out of the fact that when a position’s onward transitions are good enough, spending the position on a submission attempt is a bad trade.

Now walk left. As γ drops, attacking spreads down the hierarchy: side control, then closed guard, then open guard and leg entanglements. At γ = 0.70 the solver attacks from six of the sixteen positions. It has become a scrambler, because with the clock going it can no longer afford the four or five exchanges that patient advancement costs.

The two grey cells are worth a look too. From mounted and back taken, the two worst positions on the board, the optimal action at moderate γ is stall. The solver has no good moves, so it plays for time and hopes. Anybody who has been flattened out under a good top player will recognize the policy.

This is the section to be most suspicious of. The action multipliers are made up. I set them by hand to be plausible, and they carry no error bars at all. Change attack’s finish multiplier from 3.4 to 6 and the crossover points in that figure slide. What survives the arbitrariness is the shape: attacking is optimal from dominant positions, advancement is optimal from neutral ones, and the boundary moves down the hierarchy as the clock tightens. That is robust to the multipliers because it’s driven by the structure of the graph. The specific value of γ where side control flips is not, and I wouldn’t quote it.

5. Levers, frames and base

Different kind of math, much firmer ground. The graph sections rest on invented probabilities; this section rests on statics, where the equations are not in dispute and only the inputs are rough. It is also the least jiu-jitsu-specific material in the article. Torque and buckling do not care which art you train, and everything below applies as written to judo, wrestling or sumo.

The armbar is a moment arm

Draw the free body diagram and the technique explains itself. Your hip is the fulcrum under the elbow, your chest pins the shoulder, your hands supply force at the wrist. Torque is force times distance, and the distance that matters is hip to grip.

Free body diagram of an armbar. Three contact points, one fulcrum. Everything a coach tells you about the position is a statement about one of the terms in τ = F·d. “Pinch your knees” is a statement about none of them, which is why it serves to stop escape rather than to generate force.
Force you must supply at the wrist to reach a given elbow torque, against the length of the lever. Elbow failure torque is highly uncertain and varies enormously between people; the three bands are a range, not a measurement.

At a hip-to-wrist lever of 25 cm, reaching roughly 60 N·m takes on the order of 240 N at the wrist, comfortably inside what a hip bridge supplies. Let the arm slip until the lever is 10 cm and the same torque needs about 600 N. The curve is a hyperbola, so the cost of a slipping arm climbs far faster than the ground you lose.

This is the whole reason “walk your hips in” and “get the elbow past your centerline” are the corrections you hear from every coach in every gym. They are instructions about d. You cannot meaningfully increase F mid-technique, because your hips are as strong as they are. The only variable available is the lever, and it is the one under your control.

Frames buckle, they don’t bend

A frame is a strut under axial compression, so the failure mode is Euler buckling and the critical load goes as one over length squared:

Pcr = π²EI / (KL)²
Critical load against effective frame length. The absolute values depend on a modulus and a second moment of area I have estimated crudely, so read the shape, not the numbers: the inverse-square law is the claim.

Halving the length of a frame roughly quadruples what it holds. Going from an extended arm at 42 cm to a tucked elbow at 20 cm is about a 4.4× gain, from the geometry alone, with no change in strength.

The inverse square also explains the failure mode, which is the part practitioners actually care about. Buckling is not gradual. Below the critical load a strut barely deflects; above it, it folds. That is exactly how a straight-arm frame behaves under a passer’s chest. It holds, it holds, and then it is gone, with no intermediate state that would let you feel it going. A bent-arm frame with the elbow tucked is not a stronger version of the same thing. It is a shorter column, and it is on a different part of a very steep curve.

Base is a support polygon

This one is borrowed wholesale from bipedal robotics, where it is completely standard. Project your contact points onto the mat and take their convex hull. That is your support polygon. You are stable while your center of mass projects inside it, and you are falling when it does not.

Support polygon, viewed from above. Sweeping is a geometry problem before it is a strength problem. Get the center of mass across an edge faster than the polygon can be redrawn.

This reframes sweeping usefully. You are not lifting a person. You are moving their center of mass across the nearest edge of their hull, or better still, removing the edge. Every sweep in the sport does one of three things, and the taxonomy is exhaustive: move the center of mass, delete a contact point, or stop a new contact point from being planted. Kuzushi is the same idea in judo vocabulary, which is the older and probably better name for it.

It also explains why a wide base is not free. A large polygon is hard to tip and slow to move. Every contact point you plant for stability is a contact point you have to pick up to advance, which is the whole tension between a passing base and a mobile one.

6. Why big people feel heavier than they are

Muscle force scales with cross-sectional area. Mass scales with volume. Under geometric similarity area goes as m²/3 while mass goes as m¹, so strength per kilogram goes as m^−1/3 and declines with size. This is standard allometry and not remotely specific to grappling, but grappling happens to be a sport where it bites.

Left: force against body mass, log-log, with the hypothetical proportional scaling for comparison. Right: strength per kilogram across the IBJJF adult weight classes, normalized to rooster.

Between rooster and ultra heavy, the ends of IBJJF’s adult men’s bracket, the model predicts the larger athlete carries roughly 19% less strength per kilogram of bodyweight. That is the quantitative version of “technique beats strength for small people,” and it is more precise than the slogan: small people are not stronger in absolute terms, they are stronger relative to the mass they have to move, which is what matters for anything involving shifting your own hips.

The flip side is the part the slogan leaves out. Absolute force still scales up with mass, and a pin is an absolute-force problem, not a relative one. The heavyweight has less strength per kilo and more strength, and which fact governs depends on whether the task is moving yourself or holding someone else down. That asymmetry is a decent first-principles explanation of why the light person’s game is built on movement and the heavy person’s on pressure.

Where the two-thirds law breaks. Geometric similarity assumes athletes of different sizes are the same shape, and they aren’t. Heavier competitors in the open classes carry a different fat-free mass fraction and different limb proportions. Real strength-versus-mass exponents measured in weightlifting cohorts come out closer to 0.7 than 0.67, and there’s a good literature arguing over it. So the 19% figure is a first-principles estimate, not a measurement. It has the right sign and roughly the right size, which is all I’d claim for it. If you wanted the real number for grappling you’d fit the exponent against open-class results, and that’s an open question I haven’t seen anybody take on.

7. How long until the tap

Submissions are time-to-event data with heavy right-censoring, because most matches end with the clock rather than a tap. That is precisely the shape of problem survival analysis was built for, and it is the most statistically respectable thing you can do with grappling data. As far as I can tell almost nobody in the sport has done it.

Rather than invent a second dataset, I generated the data from the chain in section 1. Each positional event consumes a gamma-distributed chunk of time, matches are censored at ten minutes, and two covariates, belt gap and weight gap, tilt the finish weights. So there is a known ground truth, and we can check whether the estimator recovers it.

Kaplan-Meier survivor curves by the position established at t = 0, with Greenwood log-log confidence bands. 41% of these matches are censored, which is the normal state of affairs for this kind of data and the reason you cannot just take mean time-to-submission.

Two minutes in, 35% of people who had their back taken have not been submitted; from standing the figure is 82%. The curves also separate in shape as well as in level. back control and mount drop steeply and then flatten, which is the signature of a hazard that is front-loaded. If a good back attack has not finished you in the first ninety seconds, your odds improve considerably. Standing declines almost linearly instead, because standing produces submission threats without being one.

The Cox proportional hazards model then puts numbers on the covariates. It is a fifteen-line implementation that maximizes the Breslow partial likelihood with scipy.optimize and takes standard errors from the numerical observed information, so it needs no survival-analysis package at all.

Hazard ratios for finishing, with 95% intervals. A hazard ratio of 1.4 means the event is 40% more likely per unit time, not that you’re 40% more likely to win.

A belt rank of advantage multiplies the finishing hazard by about 1.41. Ten points of positional value is worth about the same. A kilogram of weight advantage is worth 1.017 per kilo, which sounds negligible until you compound it across a weight class.

The estimate is biased, and the bias is instructive. I simulated the belt effect at β = 0.45. The Cox model recovered 0.34, attenuated by about a quarter, with a confidence interval nowhere near the truth. The cause is informative censoring. When I fit “time to submitting my opponent,” a match where I got submitted is treated as censored, as though I might still have gone on to finish. But getting submitted is not neutral with respect to my chance of finishing. It is the opposite outcome. Censoring on a competing event biases the estimate toward zero, always. The fix is a competing-risks model: cause-specific hazards if you want mechanism, Fine-Gray if you want to predict cumulative incidence. If you take one methodological thing from this article, take this one, because a submission-only survival analysis of real BJJ data will be wrong in exactly this way and it will not look wrong.

8. Grip fighting has no best answer

Passing is simultaneous and adversarial: you commit to a pass, they commit to a guard, and neither sees the other’s choice in time. That is a normal-form zero-sum game, and it has no solution in pure strategies.

Four passes against four guards, entries being the passer’s probability of clearing the legs, built so that every pass has a guard that eats it. Solve by linear programming for the maximin mixed strategy.

Payoff matrix and the passer’s equilibrium mixing weights. The game value is 0.44, which is what a passer who mixes correctly achieves against a guard player who also mixes correctly.

The best single pass in the matrix is the leg drag, and a passer who only ever leg drags gets held to 0.35 by a guard player who knows it. Mixing correctly gets 0.44. Unpredictability is worth nine percentage points, and it is worth that much without a single technical improvement. Same four passes, same skill, different distribution over them.

This is why a rounded B-game beats a sharper A-game at high level, and the reason is arithmetical. Your opponent’s counter-strategy is a function of your distribution. A one-dimensional passer hands them a much easier optimization problem than a three-dimensional one.

Two more things worth noticing, both of which are the kind of insight you only get from actually solving the thing. The torreando gets weight zero in equilibrium, because it is strictly dominated here. It performs respectably on average and is still never the right component of a mix. And on the other side, reverse de la Riva also gets zero, so the equilibrium is supported on three guards and three passes out of four each. Equilibrium strategies are usually sparse, which means “mix everything” is the wrong lesson. The right one is “mix over the handful of things that survive.”

What this model leaves out. Real grip fighting is sequential rather than simultaneous. You see the grip before you commit, which makes it an extensive-form game with information sets, and feints acquire value that a normal-form model cannot represent. Payoffs also aren’t stationary: they depend on fatigue, the score, and what you did ninety seconds ago. Collar sleeve is also a gi-only guard, so the matrix as written is implicitly a gi match, even though three of the four passes work perfectly well without one. And the sixteen entries are invented, like everything else in the graph sections. The conclusion that mixing beats your best pure strategy holds for any matrix with no saddle point, so it survives. The specific weights do not.

9. Leg entanglements and the shape of knots

Leg entanglements are configurations of four limbs that cross each other, which makes them look a great deal like braids. This is the section where every quantitative jiu-jitsu article goes wrong, so let me be precise about what is and is not being claimed.

A leg entanglement as a braid word. Strands are limbs, crossings are generators, and σi−1 is the same crossing with the other strand on top. Reading order is top to bottom.

Knot theory studies closed curves. Limbs are open arcs attached to a body, so nothing here is a knot, and any claim about the “knot invariants of 50/50” is nonsense. The right objects are tangles and braids, which handle open strands with fixed endpoints, and even then limbs are thick, jointed, and cannot pass through each other in ways that idealized strands can. So: an analogy with real structure, not a theorem.

What the analogy buys you is a well-posed question. Braid theory cares about which configurations are related by isotopy, meaning continuous deformation without cutting. In a leg entanglement, cutting means releasing the configuration. So: which entanglements can you get to without letting go? That is a graph reachability problem, and it is completely answerable.

The entanglement graph. Solid edges reconfigure without releasing; dashed edges require letting go. The heavy path is the shortest no-release route from 50/50 to reverse 50/50.

Take the question everyone actually asks: can you get from 50/50 to reverse 50/50 without releasing? In this graph, yes, but it takes five steps, through inside sankaku, saddle (411), outside ashi and cross ashi. Release the legs and it is one step.

That gap between five and 1 is the interesting quantity, and it is the closest thing in this article to a new way of looking at a position. It is the cost of the shortcut, in the currency of control. A five-step isotopy keeps your opponent entangled the entire way and never gives them a moment of free legs; the one-step release is fast and hands them an exit. Most of what distinguishes leg lockers from people who merely know leg locks is knowing which entanglement pairs have a cheap isotopy between them and which do not, and that is exactly the data this graph holds.

One legality note, because these entanglements are not universally available. Most of them exist to set up heel hooks, which IBJJF bans in the gi at every belt and permits in no-gi only for adult brown and black belts. ADCC and most submission-only promotions allow them outright. So this section describes a submission ruleset more than a points one, and if you compete in the gi there are nodes on that graph you can enter but cannot finish from.

Build this graph for your own game and it becomes a training document. Isolated nodes are entanglements you can enter and not leave. Bridges are the positions that hold your system together. Long isotopy distances between things you use a lot are gaps to drill.

10. What it would take to do this for real

Everything above is a method demonstration on invented weights. The obvious next move is to replace the weights with counts. This section is about how much work that actually is, because the answer surprised me and it is the most practically useful thing here.

Transition counts out of a position are multinomial, so with a Dirichlet prior the posterior for each transition probability is Beta and the credible intervals are closed form. That lets you ask the sample size question directly.

Credible interval half-width against the number of times you’ve observed a position, under a Jeffreys prior. The horizontal line is ±5 percentage points, which is about the resolution you need before the positional hierarchy is stable.

To pin a transition probability near 0.3 to within five points you need about 321 visits to that position. That counts occurrences of the position, not matches played. For a position appearing in a fifth of matches, it works out at something like 1,600 matches tagged, and you need that for all sixteen states.

That is a real project. It is also why the interesting contribution is not the analysis, which is a few hundred lines, but the encoding scheme.

The encoding is the hard part

“What counts as a distinct position” is the actual research question, and the sixteen states I used are a defensible but arbitrary answer. Is north-south its own state or a variant of side control? At what point does an open guard become a leg entanglement? If a pass is reversed mid-motion, was there ever a transition? Every one of these is a judgement call, every one changes the numbers, and none of them has a right answer. Coarsen your state space and you smear real distinctions; refine it and every cell is starved of data. That tension is the whole design problem.

Some practical notes, if you do it:

  • Tag transitions, not techniques. The graph needs edges, and “he passed to side control” is an edge whereas “knee slice” is a label on one.
  • Write the ambiguity rules down before you start, and tag a hundred events twice to measure your own agreement with yourself. If you cannot reproduce your own tags, the state space is too fine.
  • Record match time on every event. It costs nothing at tagging time and it is the difference between a plain chain and everything in section 7.
  • Tag whole matches, never highlights. Highlight reels are sampled on the outcome, which biases every transition probability in the direction of whatever is exciting.

The bias you cannot tag your way out of

Competition footage is elite people under a points ruleset. The resulting matrix describes IBJJF black belts and not your Tuesday night, and those are different sports in ways that matter: stalling is rewarded differently, leg locks are legal or not, the clock is a different length. Fifty of your own rolls, hand-tagged, gives you a smaller and dirtier dataset that is actually about the thing you want to know. Both are worth having and they answer different questions.

To sum up the whole exercise, the math is easy and mostly off the shelf. The modelling choices (state space, what an action is, which ruleset, whose matches) are where all the difficulty lives, and they are choices a practitioner is better placed to make than a mathematician. Which is, I think, the reason to write the article.

Every figure generated by bjj_model.py and bjj_figures.py, using numpy, scipy, networkx and matplotlib, with no data files and no network. Transition weights are hand-set priors; replace WEIGHTS with your own counts and the entire article recomputes.

About the Author

Brian Curry is a Kansas City–based Machine Learning & AI Strategist, Marketing Economist, and Applied Researcher. He drives Vector1, a personal open-source research initiative advancing marketing economics, causal inference, cognitive AI architectures, and the economics of intelligent automation.

He has led advanced data science and analytics initiatives across major enterprises including Koch Industries, Tractor Supply, Hallmark, Garmin, and AT&T. His work applies economic thinking and causal inference to connect marketing investment to outcomes, operational decisions to economic value, and AI adoption to competitive advantage.

Key Research & Open-Source Projects (GitHub: @Bodhi8):

  • PyCausalSim — A Python framework for causal discovery and inference through simulation. Enables testing of counterfactual scenarios to identify true structural drivers.
  • Papilon — A complex system and causal discovery library for Media Mix Modeling (MMM) and adaptive systems.
  • Regimecast — Applied research and tooling for time-series regime detection and forecasting.
  • CarryNet — Open-source research and modeling focused on commodity carry trade analysis.
  • Foodmesh-ml — Machine learning, optimization, and simulation framework for modeling distributed food systems.
  • Memory-Node Encapsulation (MNE) — A cognitive AI architecture and novel data structure for artificial episodic memory.
  • The Autonomy Economy — A macroeconomic framework projecting the dual impacts of massive productivity growth and extreme price deflation in an AI-automated world.
  • Meaning-Flow — Research focused on semantic content modeling and automated coverage gap analysis.

Contact: brian at vector1.ai | Vector1 | GitHub | LinkedIn

This article may be shared with attribution under Creative Commons Attribution 4.0 International (CC BY 4.0).

Related research