Examples / JEE Advanced
Problem 1: JEE (Advanced) 2022
JEE (Advanced) 2022, Paper 1, question 10, written as a fang program: the diamond of eight 1 Ω resistors and two batteries below, with each of its four claimed currents a constraint the kernel decides.

This is the one example that is not a board, and it is here because the question is the same one a board asks all day: is this claim about my circuit true? The kernel answers it the way it answers any other: the potentials are a value, Kirchhoff’s current law is a constraint, and each of the four claims is a constraint the checker decides. All four hold.
The circuit
Section titled “The circuit”The four corners are left, top, right and bottom. R6 and R7 are the upper sides, R5 and R8 the lower ones, and R2 and R4 run down the vertical axis into the centre. The middle row runs
left corner -- ε₂ -- R3 -- centre -- ε₁ -- R1 -- right cornerwith both batteries pointing the same way, so each lifts the node on its right.
GND1 is the centre: a one-terminal part that marks the node the potentials
are measured against, and adds nothing to it.
The program
Section titled “The program”problem_1.py claims five numbers and nothing else:
v_centre = Parameter("V", default=0 * V, description="the reference node")v_left = Parameter("V", default=-1.2 * V, description="the left corner")v_top = Parameter("V", default=1.2 * V, description="the top corner")v_right = Parameter("V", default=4.8 * V, description="the right corner")v_bottom = Parameter("V", default=1.2 * V, description="the bottom corner")Every current below is derived from those by Ohm’s law, so there is one claim to check and not thirteen. Kirchhoff’s current law is then written once per node, and the four statements the paper asks about are written beside them in the same form, a constraint and not a comment:
require(equals(total(into_centre, from_left_to_top, from_left_to_bottom), no_current))...require(equals(out_of_centre, 7.2 * A)) # (A) 7.2 A through R1A parameter reference builds an expression node from one operator, and Kirchhoff needs them nested, so the tree is written out with three small helpers. That is not a workaround: every node checks its own dimensions as it is constructed, so a term that divides a potential by the wrong parameter is rejected where it is written rather than where it is evaluated.
Five node equations and four claims: nine constraints, and the checker decides all nine and fails none. Move any potential and the node equations fail first, which is what makes the four claims worth anything.
Where the potentials came from
Section titled “Where the potentials came from”They were not solved in the program. The kernel checks claims; it does not
solve linear systems. solve.py elaborates the same graph, has
fang.simulation compile a plan and lower it to SPICE, and runs ngspice on the
deck fang wrote:
R1 2 3 1 V1 2 0 12R2 4 0 1 V2 5 7 6...GND1 marks a node and is not a device, so it carries no simulation model, and
a plan that reaches a component with no model is rejected rather than run with
a stand-in. Naming it as abstracted is what lets the plan compile, and the plan
then says so in its own assumptions:
assumption: CMP-fe129c51b056 is abstracted: it contributes no device to the netlistThe operating point gives all eight branch currents (R1 7.2 A, R2 1.2 A, R3 4.8 A, R4 1.2 A, R5 2.4 A, R6 2.4 A, R7 3.6 A, R8 3.6 A) and the five potentials go into the program, where the constraints judge them. The two paths are independent: ngspice solves, the kernel decides.
The question and the answer are in the graph too
Section titled “The question and the answer are in the graph too”A netlist says what the circuit is. It does not say what was asked of it, or
what came back. Both are entities here. The question and its four options are
Cites, the answer is Requires, the numbers are Calculates, and a
Verifies closes the requirement, so out/rationale.md
carries them out of the program without anything being retyped:
system.answer: All four options hold: (A), (B), (C) and (D) MUST, state KNOWN, validation by analysis. Verified by
system.answered: PASS by analysis
with each option scored against the branch it names:
(A) r1 7.2 A: correct; (B) r2 1.2 A: correct; (C) r3 4.8 A: correct; (D) r5 2.4 A: correct.
and every resistor with its value and its current beside it:
every resistor is 1 Ω: r1 7.2 A, r2 1.2 A, r3 4.8 A, r4 1.2 A, r5 2.4 A, r6 2.4 A, r7 3.6 A, r8 3.6 A
What comes out
Section titled “What comes out”11 parts, 7 nets, 99 entities, 9 checks, none failed and none undecided.
out/problem_1.net: the KiCad netlistout/netlist.txt: the same projection as text, with the two batteries carrying 12 V and 6 V as their valuesout/checks.txt: nine checks, all decidedout/graph.txt: 99 entities, by kindout/rationale.md: the question, its four options, every resistor’s value and current, and the answer, each one an entity, not prose
The schematic is drawn by copperhead’s
drafting engine from this circuit’s netlist, with KiCad’s own library symbols,
and it opens in KiCad as figure/problem_1.kicad_sch.
KiCad reads back from the sheet exactly the connections the circuit has;
draw_figures.py refuses to write one that does not.
The engine is built for amplifier stages, and it draws this diamond as pieces
joined by net labels rather than as the square the paper draws, so read it as
the netlist reads: R1 sits between Net-(R1-Pad1), the junction inside the
middle row, and Net-(R1-Pad2), the right corner, and every ground symbol is
the centre.
The interconnect view is the other picture, and it answers a different
question: it is fang’s own projection, drawn by fang view, and it names the
parts the way the program does. r1 through r8 are the resistors the paper
labels R₁ through R₈, and the two junctions inside the middle row (between
each battery and the resistor in series with it) are the two nets with only
two pads on them.
Running it
Section titled “Running it”fang check examples/jee_advanced/problem_1/problem_1.pyfang netlist examples/jee_advanced/problem_1/problem_1.pyfang view examples/jee_advanced/problem_1/problem_1.py interconnect -o interconnect.svgpython examples/draw_figures.py problem_1 # needs copperhead and kicad-cli
python examples/jee_advanced/problem_1/solve.py # needs ngspice on PATHThe whole program
Section titled “The whole program”"""Eight 1 ohm resistors, two ideal batteries, and the question they pose.Show 20 more lines
JEE (Advanced) 2022, Paper 1: a diamond with four corners and a centre node.R6 and R7 are the upper sides, R5 and R8 the lower ones; R2 and R4 run down thevertical axis into the centre. The middle row runs
left corner -- e2 -- R3 -- centre -- e1 -- R1 -- right corner
and both batteries point the same way, so each lifts the node on its right.The paper asks which of four claimed currents are correct.
This example is not a board. It is here because the question a board asks allday -- "is this claim about my circuit true?" -- is the question a physicspaper asks once, and the kernel answers it the same way: the potentials are avalue, Kirchhoff's current law is a constraint, and each of the four claims isa constraint the checker decides rather than a comment nobody re-reads.
The potentials below were not solved here. `solve.py` beside this file lowersthe graph to SPICE through `fang.simulation` and runs ngspice on it; thenumbers it returns are written in, and the constraints are what judge them."""
from fang.constraints import Arithmetic, Comparison, Literal, Nodefrom fang.interfaces import Pin, PinMapfrom fang.lang import ( A, Electrical, Ohm, Parameter, ParameterRef, Part, System, V, require,)from fang.parts import Resistor, TwoPinfrom fang.rationale import Calculates, Cites, Requires, Verifies
# --------------------------------------------------------------------------# Writing the expression tree out# --------------------------------------------------------------------------## A parameter reference builds a node from one operator: `a - b` is an# expression, and so is `a / b`. Kirchhoff needs them nested, and a node is not# itself an operand of Python's operators, so the tree is written out. That is# not a workaround. Every node checks its own dimensions as it is constructed,# so a term that divides a voltage by the wrong parameter is rejected where it# is written, not where it is evaluated.
def _node(value) -> Node: if isinstance(value, Node): return value if isinstance(value, ParameterRef): return value._node() return Literal.of(value)
def total(*terms) -> Arithmetic: """The sum of the currents named. Dimensions must agree.""" return Arithmetic("add", tuple(_node(term) for term in terms))
def across(here, there) -> Arithmetic: """The potential difference from one node to another.""" return Arithmetic("sub", (_node(here), _node(there)))
def through(difference, resistance) -> Arithmetic: """Ohm's law: the current a difference drives through a resistance.""" return Arithmetic("div", (_node(difference), _node(resistance)))
def equals(left, right) -> Comparison: return Comparison("eq", (_node(left), _node(right)))
# --------------------------------------------------------------------------# The parts# --------------------------------------------------------------------------
class Battery(TwoPin): """An ideal EMF, no internal resistance. Pin 1 is the positive terminal."""
designator_prefix = "V" voltage = Parameter("V")
class GroundReference(Part): """The node every potential below is measured against.Show 4 more lines
One terminal and no value: it marks a node rather than adding anything to it, and a one-terminal part emits no device into a simulation deck. """
designator_prefix = "GND"
node = Electrical() PIN1 = Pin("1", role="ground", number="1") pinmap = PinMap({"node.line": "1"})
class Bridge(System): """The figure, then the claim, then the law that judges it."""
# -- the question, its options, and the answer ------------------------- # # A netlist says what the circuit is; it does not say what was asked of it # or what came back. Both are entities here, so `out/rationale.md` carries # them out of the program and nothing has to be retyped to say what this # example concluded.
question = Cites( "Which of the following statement(s) is(are) correct? " "(A) the current through R1 is 7.2 A; (B) the current through R2 is " "1.2 A; (C) the current through R3 is 4.8 A; (D) the current through " "R5 is 2.4 A", document="JEE (Advanced) 2022, Paper 1", locator="question 10, multiple correct, four options", )
answer = Requires( "All four options hold: (A), (B), (C) and (D)", priority="MUST", validation="analysis", )
# -- the numbers behind it ----------------------------------------------
node_potentials = Calculates( "the operating point, taken against the centre node", inputs=("e1", "e2", "reference"), result=( "centre 0 V, left -1.2 V, top 1.2 V, right 4.8 V, bottom 1.2 V, " "from batteries of 12 V and 6 V" ), requirements=("answer",), )
branch_currents = Calculates( "I = (V_here - V_there) / R, once per branch", inputs=("r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8"), result=( "every resistor is 1 ohm: r1 7.2 A, r2 1.2 A, r3 4.8 A, " "r4 1.2 A, r5 2.4 A, r6 2.4 A, r7 3.6 A, r8 3.6 A" ), requirements=("answer",), )
options = Calculates( "each claimed current against the branch it names", inputs=("r1", "r2", "r3", "r5"), result=( "(A) r1 7.2 A: correct; (B) r2 1.2 A: correct; " "(C) r3 4.8 A: correct; (D) r5 2.4 A: correct. " "All four options are right, which is what the paper's key says" ), requirements=("answer",), )
operating_point = Cites( "ngspice reports 7.2, 1.2, 4.8, 1.2, 2.4, 2.4, 3.6 and 3.6 A " "through R1 to R8", document="examples/jee_advanced/problem_1/solve.py", locator="the operating point fang.simulation lowered and ran", )
answered = Verifies( "answer", method="analysis", evidence=("question", "operating_point"), result="PASS", )
# The candidate answer, and the whole of it: every current below is derived # from these five numbers by Ohm's law, so there is one claim to check and # not thirteen. v_centre = Parameter("V", default=0 * V, description="the reference node") v_left = Parameter("V", default=-1.2 * V, description="the left corner") v_top = Parameter("V", default=1.2 * V, description="the top corner") v_right = Parameter("V", default=4.8 * V, description="the right corner") v_bottom = Parameter("V", default=1.2 * V, description="the bottom corner")
e1 = Battery(voltage=12 * V, package="Battery") e2 = Battery(voltage=6 * V, package="Battery")
r1 = Resistor(resistance=1 * Ohm, package="R_0805") r2 = Resistor(resistance=1 * Ohm, package="R_0805") r3 = Resistor(resistance=1 * Ohm, package="R_0805") r4 = Resistor(resistance=1 * Ohm, package="R_0805") r5 = Resistor(resistance=1 * Ohm, package="R_0805") r6 = Resistor(resistance=1 * Ohm, package="R_0805") r7 = Resistor(resistance=1 * Ohm, package="R_0805") r8 = Resistor(resistance=1 * Ohm, package="R_0805")
reference = GroundReference(package="GND")
def architecture(self): # Left corner: the two left-hand sides, and the negative end of e2. self.r6.p1 >> self.r5.p1 self.r5.p1 >> self.e2.p2
# Top corner. self.r6.p2 >> self.r7.p1 self.r7.p1 >> self.r2.p1
# Right corner. self.r7.p2 >> self.r8.p2 self.r8.p2 >> self.r1.p2
# Bottom corner. self.r5.p2 >> self.r8.p1 self.r8.p1 >> self.r4.p2
# Centre, and the reference the potentials are taken against. self.r2.p2 >> self.r4.p1 self.r4.p1 >> self.r3.p2 self.r3.p2 >> self.e1.p2 self.e1.p2 >> self.reference.node
# The two junctions inside the middle row, between battery and resistor. self.e1.p1 >> self.r1.p1 self.e2.p1 >> self.r3.p1
# -- the branch currents, each read as leaving the first node named -----
def _left_into_centre(self): """Left corner -> e2 -> R3 -> centre. e2 lifts the node between them.""" return through( across(total(self.v_left, self.e2.voltage), self.v_centre), self.r3.resistance, )
def _centre_into_right(self): """Centre -> e1 -> R1 -> right corner.""" return through( across(total(self.v_centre, self.e1.voltage), self.v_right), self.r1.resistance, )
def constraints(self): no_current = 0 * A
from_centre_to_bottom = through( across(self.v_centre, self.v_bottom), self.r4.resistance ) from_top_to_centre = through( across(self.v_top, self.v_centre), self.r2.resistance ) from_left_to_top = through( across(self.v_left, self.v_top), self.r6.resistance ) from_top_to_right = through( across(self.v_top, self.v_right), self.r7.resistance ) from_left_to_bottom = through( across(self.v_left, self.v_bottom), self.r5.resistance ) from_bottom_to_right = through( across(self.v_bottom, self.v_right), self.r8.resistance )
into_centre = self._left_into_centre() out_of_centre = self._centre_into_right()
# Kirchhoff's current law, once per node. The centre is implied by the # other four and is written anyway: a redundant check that agrees is # worth more than one that was left out. require( equals( total(into_centre, from_left_to_top, from_left_to_bottom), no_current ) ) require(equals(total(from_top_to_centre, from_top_to_right), from_left_to_top)) require( equals( total(out_of_centre, from_top_to_right, from_bottom_to_right), no_current, ) ) require( equals( total(from_left_to_bottom, from_centre_to_bottom), from_bottom_to_right, ) ) require( equals( total(into_centre, from_top_to_centre), total(out_of_centre, from_centre_to_bottom), ) )
# The four statements the paper asks about. Each is written in the # direction the current actually flows, which is what makes the # magnitude the paper asks for the value on the left. require(equals(out_of_centre, 7.2 * A)) # (A) 7.2 A through R1 require(equals(from_top_to_centre, 1.2 * A)) # (B) 1.2 A through R2 require(equals(into_centre, 4.8 * A)) # (C) 4.8 A through R3 require( # (D) 2.4 A through R5 equals(through(across(self.v_bottom, self.v_left), self.r5.resistance), 2.4 * A) )The files it writes
Section titled “The files it writes”The parts, then the nets and the pads on them.
GND1 GroundReference Package:GNDR1 1 Ohm Package:R_0805R2 1 Ohm Package:R_0805R3 1 Ohm Package:R_0805R4 1 Ohm Package:R_0805R5 1 Ohm Package:R_0805R6 1 Ohm Package:R_0805R7 1 Ohm Package:R_0805R8 1 Ohm Package:R_0805V1 12 V Package:BatteryV2 6 V Package:BatteryNet-(GND1-Pad1) GND1.1 R2.2 R3.2 R4.1 V1.2Net-(R1-Pad1) R1.1 V1.1Net-(R1-Pad2) R1.2 R7.2 R8.2Net-(R2-Pad1) R2.1 R6.2 R7.1Net-(R3-Pad1) R3.1 V2.1Net-(R4-Pad2) R4.2 R5.2 R8.1Net-(R5-Pad1) R5.1 R6.1 V2.2Every check that ran, and every one left undecided.
9 checks, 0 failed, 0 undecidedWhat the elaborated graph contains, by entity kind.
1 block 3 calculation 11 component 28 connection 9 constraint 2 evidence 1 interface 21 pin 21 port 1 requirement 1 verification 99 totalsnapshot sha256:02ff7f02837d33a59aa488d9d29f9e228673f664c3cbe3d108ad778b1436f073All of it, including the KiCad netlist, is in
examples/jee_advanced/problem_1/out/. Rebuild it with:
fang build examples/jee_advanced/problem_1/problem_1.py