Skip to content
copperhead.sh
Get started

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.

the question as the paper prints it
The question as the paper prints it

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 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 corner

with 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.

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 R1

A 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.

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 12
R2 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 netlist

The 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

11 parts, 7 nets, 99 entities, 9 checks, none failed and none undecided.

the schematic, drawn by copperhead from the circuit's netlist
The schematic, drawn by copperhead from the circuit's netlist

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, fang's own projection
The interconnect view, fang's own projection

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.

Terminal window
fang check examples/jee_advanced/problem_1/problem_1.py
fang netlist examples/jee_advanced/problem_1/problem_1.py
fang view examples/jee_advanced/problem_1/problem_1.py interconnect -o interconnect.svg
python examples/draw_figures.py problem_1 # needs copperhead and kicad-cli
python examples/jee_advanced/problem_1/solve.py # needs ngspice on PATH
examples/jee_advanced/problem_1/problem_1.py
"""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 the
vertical 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 all
day -- "is this claim about my circuit true?" -- is the question a physics
paper asks once, and the kernel answers it the same way: the potentials are a
value, Kirchhoff's current law is a constraint, and each of the four claims is
a constraint the checker decides rather than a comment nobody re-reads.
The potentials below were not solved here. `solve.py` beside this file lowers
the graph to SPICE through `fang.simulation` and runs ngspice on it; the
numbers it returns are written in, and the constraints are what judge them.
"""
from fang.constraints import Arithmetic, Comparison, Literal, Node
from fang.interfaces import Pin, PinMap
from fang.lang import (
A,
Electrical,
Ohm,
Parameter,
ParameterRef,
Part,
System,
V,
require,
)
from fang.parts import Resistor, TwoPin
from 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 parts, then the nets and the pads on them.

out/netlist.txt
GND1 GroundReference Package:GND
R1 1 Ohm Package:R_0805
R2 1 Ohm Package:R_0805
R3 1 Ohm Package:R_0805
R4 1 Ohm Package:R_0805
R5 1 Ohm Package:R_0805
R6 1 Ohm Package:R_0805
R7 1 Ohm Package:R_0805
R8 1 Ohm Package:R_0805
V1 12 V Package:Battery
V2 6 V Package:Battery
Net-(GND1-Pad1) GND1.1 R2.2 R3.2 R4.1 V1.2
Net-(R1-Pad1) R1.1 V1.1
Net-(R1-Pad2) R1.2 R7.2 R8.2
Net-(R2-Pad1) R2.1 R6.2 R7.1
Net-(R3-Pad1) R3.1 V2.1
Net-(R4-Pad2) R4.2 R5.2 R8.1
Net-(R5-Pad1) R5.1 R6.1 V2.2

Every check that ran, and every one left undecided.

out/checks.txt
9 checks, 0 failed, 0 undecided

What the elaborated graph contains, by entity kind.

out/graph.txt
1 block
3 calculation
11 component
28 connection
9 constraint
2 evidence
1 interface
21 pin
21 port
1 requirement
1 verification
99 total
snapshot sha256:02ff7f02837d33a59aa488d9d29f9e228673f664c3cbe3d108ad778b1436f073

All of it, including the KiCad netlist, is in examples/jee_advanced/problem_1/out/. Rebuild it with:

Terminal window
fang build examples/jee_advanced/problem_1/problem_1.py