Lecture 2-2: Equivalences and Path Algebra

mvrnote: intro

Sections and Retracts

A function f : A → B lets us transform data of type A into data of type B, so we can see a function f : A → B as a way to represent elements of A by elements of B.

For example, in many languages, it is common to represent Booleans by numbers by representing false by 0 and true by 1. In Agda, we could write this a function Bool → ℕ that turns each Boolean into its representation..

Bool→ℕ : Bool  
Bool→ℕ true = suc zero
Bool→ℕ false = zero

If this is going to be useful, it needs to faithfully represent Boolean values, in that we are able to decode a natural number back into a Boolean.

isPositive :   Bool
isPositive zero = false
isPositive (suc n) = true

isPositive-represents-Bool : (b : Bool)  isPositive (Bool→ℕ b)  b
-- Exercise:
-- isPositive-represents-Bool b = {!!}

This is a common situation, where a function f : A → B has a one-sided inverse g : B → A so that f (g b) ≡ b. The technical name for this is that g is a section of f.

isSection : {A : Type } {B : Type ℓ'}
   (f : A  B) 
   (g : B  A)
   Type ℓ'
isSection {B = B} f g = (b : B)  f (g b)  b

So here, Bool→ℕ is a section of isPositive; this is exactly what isPositive-represents-Bool above is saying.

isSection-isPositive-Bool→ℕ : isSection isPositive Bool→ℕ
isSection-isPositive-Bool→ℕ = isPositive-represents-Bool

One way to justify the name “section” is thinking of the the type B (here Bool) as being smaller than the type A (here ). A function is a section if it picks out a small part of A (a small “section” of A) that has the shape of B.

This is a notion that is going to come up a lot, so we will package the notion of section into a record type.

record SectionOf {A : Type } {B : Type ℓ'} (f : A  B) : Type (ℓ-max  ℓ') where
  constructor sectionData
  field
    map : B  A
    proof : isSection f map

open SectionOf public

While every Boolean b can be represented by the natural number Bool→ℕ b, it is not the case that every natural number a can be represented by a Boolean with respect to the function Bool→ℕ. A natural number gives more data than a Boolean.

But what about the following type RedOrBlue?

data RedOrBlue : Type where
  red : RedOrBlue
  blue : RedOrBlue

It is quite apparent that RedOrBlue has the same data as Bool: each has two elements and nothing more. We can represent a Bool as an element of RedOrBlue as follows:

Bool→RedOrBlue : Bool  RedOrBlue
Bool→RedOrBlue true = red
Bool→RedOrBlue false = blue

And we can show this is a faithful representation by giving a section RedOrBlue→Bool of Bool→RedOrBlue.

RedOrBlue→Bool : RedOrBlue  Bool
RedOrBlue→Bool red = true
RedOrBlue→Bool blue = false

isSection-Bool-RedOrBlue : isSection Bool→RedOrBlue RedOrBlue→Bool
isSection-Bool-RedOrBlue red = refl
isSection-Bool-RedOrBlue blue = refl

But this time, the function RedOrBlue→Bool faithfully represents an element of RedOrBlue as a Boolean too!

isSection-RedOrBlue-Bool : isSection RedOrBlue→Bool Bool→RedOrBlue
isSection-RedOrBlue-Bool true = refl
isSection-RedOrBlue-Bool false = refl

For this reversed situation, we say that f : A → B is a retract when it has a section.

isRetract : {A : Type } {B : Type ℓ'}
   (f : A  B) 
   (g : B  A)
   Type 
isRetract f g = isSection g f

record RetractOf {A : Type } {B : Type ℓ'} (f : A  B) : Type (ℓ-max  ℓ') where
  constructor retractData
  field
    map : B  A
    proof : isRetract f map

open RetractOf public

So as well as isSection-Bool-RedOrBlue, we also have:

isRetract-Bool-RedOrBlue : isRetract Bool→RedOrBlue RedOrBlue→Bool
isRetract-Bool-RedOrBlue = isSection-RedOrBlue-Bool

And another way of summarising the Bool and situation is that isPositive is a retract of Bool→ℕ.

isRetract-Bool→ℕ-isPositive : isRetract Bool→ℕ isPositive
isRetract-Bool→ℕ-isPositive = isPositive-represents-Bool

Again, thinking of Bool as being smaller than , a function is a retract when it is describing a way to shrink, or “retract”, the larger type into the smaller type.

Equivalences

When the function f : A → B has a section g : B → A and has a retract g' : B → A, as is the case for RedOrBlue→Bool, we say that f is an equivalence.

In this situation, f faithfully represents elements of A as elements of B (which we know because it has a section g), and g' faithfully represents elements of B as elements of A (which we know because it has a section f, i.e. is a retract of f). So, we can represent elements of B by elements of A and vice-versa — the types describe equivalent data.

We will package all this information into some handy records.

record isEquiv {A : Type } {B : Type ℓ'} (f : A  B) : Type (ℓ-max  ℓ') where
  constructor isEquivData
  field
    section : SectionOf f
    retract : RetractOf f

open isEquiv public

record Equiv (A : Type ) (B : Type ℓ') : Type (ℓ-max  ℓ') where
  constructor equiv
  field
    map : A  B
    proof : isEquiv map

open Equiv public

To avoid typing Equiv out everywhere, we will use the syntax A ≃ B for the type of equivalences between A and B.

_≃_ : (A : Type ) (B : Type ℓ')  Type (ℓ-max  ℓ')
_≃_ = Equiv

infix 4 _≃_

To make these less annoying to work with, we’ll write some helpers for constructing these Equivs.

pattern packIsEquiv sec isSec ret isRet
  = isEquivData (sectionData sec isSec) (retractData ret isRet)

pattern packEquiv fun sec isSec ret isRet
  = equiv fun (packIsEquiv sec isSec ret isRet)

We mentioned these pattern synonyms back in Lecture 1-X. Here, they let us construct an element of an Equiv record by writing packEquiv fun sec isSec ret isRet rather than the fully explicit equiv fun (isEquivData (sectionData sec isSec) (retractData ret isRet)).

An equivalence between two types says, in effect, that elements of those types are different representations of the same data. Putting together the maps we defined above, Bool is equivalent to RedOrBlue

Bool≃RedOrBlue : Bool  RedOrBlue
Bool≃RedOrBlue 
  = packEquiv Bool→RedOrBlue 
              RedOrBlue→Bool
              isSection-Bool-RedOrBlue
              RedOrBlue→Bool
              isRetract-Bool-RedOrBlue

Having the section of f and retract of f be the same map is a very common situation, so we will use a helper to duplicate the backwards map in this case.

inv→equiv : (fun : A  B)
       (inv : B  A)
       (isSec : isSection fun inv)
       (isRet  : isRetract fun inv)
       A  B
inv→equiv fun inv isSec isRet = packEquiv fun inv isSec inv isRet

Aside: It might seem strange that our notion of equivalence involves two maps backwards rather than just one.

When a map has a single inverse map that is a both a section and a retract, the map is called an isomorphism, a faux-Greek word meaning “same shape”. While every isomorphism gives rise to an equivalence (via the function inv→equiv we just defined) and every equivalence gives rise to an isomorphism (via invEquiv coming up in Lecture 2-X), the type of equivalences and the type of isomorphisms between two types are not always the same!

It will turn out that “equivalence” as we’ve defined it here is the better notion, because the type isEquiv f is a proposition about the function f (see isProp-isEquiv), whereas being an “isomorphism” sneaks in extra data (see ¬isProp-isIso). We will happily forget about the term “isomorphism” from this point on and stick with equivalence.

At the very least, we can show that the identity function on any type is an equivalence.

isEquiv-idfun : isEquiv (idfun {A = A})
-- Exercise:
-- isEquiv-idfun .section .map = {!!}
-- isEquiv-idfun .section .proof x = {!!}
-- isEquiv-idfun .retract .map = {!!}
-- isEquiv-idfun .retract .proof x = {!!}

idEquiv : (A : Type )  A  A
-- Exercise:
-- idEquiv = {!!}

Now, this isn’t the only way we could have shown that Bool is equivalent to RedOrBlue; we could also have sent true to blue and false to red. Define this other equivalence below:

OtherBool≃RedOrBlue : Bool  RedOrBlue
OtherBool≃RedOrBlue = inv→equiv to fro to-fro fro-to
  -- Exercise:
  -- where
  --   to : Bool → RedOrBlue
  --   to x = {!!}
  --
  --   fro : RedOrBlue → Bool
  --   fro x = {!!}
  --
  --   to-fro : isSection to fro
  --   to-fro x = {!!}
  --
  --   fro-to : isRetract to fro
  --   fro-to x = {!!}

Not every function Bool → RedOrBlue is an equivalence. If we send both true and false to red, for example, then there is no way we can find an inverse. Any section would have to send red to true and also to false, but these aren’t equal.

In Lecture 1-X, we had a few “bijections” between types. At the time, all we could do is produce maps going each way. Now we can show that these really are equivalences. Here’s an especially easy one, where the paths in the to-fro and fro-to functions can be refl for any argument.

-- mvrnote: unused
×-ump-≃ : (C  A) × (C  B)  (C  A × B)
×-ump-≃ = inv→equiv to fro to-fro fro-to
  where
    -- We defined this way back in Lecture 1-1, but only for
    -- types in the lowest universe.
    to : (C  A) × (C  B)  (C  A × B)
    to (f , g) c = (f c , g c)

    fro : (C  A × B)  (C  A) × (C  B)
    fro h =  c  fst (h c)) ,  c  snd (h c))

    to-fro : isSection to fro
--  Exercise:
--  to-fro x = {!!}

    fro-to : isRetract to fro
--  Exercise:
--  fro-to x = {!!}

-- mvrnote: unused
×-assoc-≃ : (A × (B × C))  ((A × B) × C)
×-assoc-≃ = inv→equiv to fro to-fro fro-to
  where
    to : (A × (B × C))  ((A × B) × C)
    to (a , (b , c)) = (a , b) , c

    fro : ((A × B) × C)  (A × (B × C))
    fro ((a , b) , c) = (a , (b , c))

    to-fro : isSection to fro
--  Exercise:
--  to-fro x = {!!}

    fro-to : isRetract to fro
--  Exercise:
--  fro-to x = {!!}

-- mvrnote: unused
curry-≃ : {ℓ₁ ℓ₂ ℓ₃ : Level}
   {A : Type ℓ₁}
   {B : A  Type ℓ₂}
   {C : (x : A)  B x  Type ℓ₃}
   ((p : Σ[ x  A ] B x)  C (p .fst) (p .snd))
   ((x : A)  (y : B x)  C x y)
-- We don't have to give names to the section and retract proofs at
-- all, if we prefer.
-- Exercise:
-- curry-≃ = equiv Σ-curry Σ-uncurry (λ x → {!!}) (λ x → {!!})

-- mvrnote: put somewhere?
funext-≃ : {A : Type } {B : A  Type ℓ'}
   {f g : (a : A)  B a}
   ((x : A)  f x  g x)
   (f  g)
funext-≃ = inv→equiv funext funext⁻  _  refl)  _  refl)

funextP-≃ : {A : Type } {B : I  Type ℓ'}
   {f : A  B i0} {g : A  B i1}
   ((x : A)  PathP B (f x) (g x))
   PathP  i  A  B i) f g
funextP-≃ = inv→equiv funextP funextP⁻  _  refl)  _  refl)

The above examples work because the composite of to and fro acts like the identity on any argument.

Another place we’ll want to do this is when pulling records apart. The uniqueness rule for records means that the section and retraction proofs are trivial.

explode-isEquiv : {f : A  B}  isEquiv f  (SectionOf f × RetractOf f)
explode-isEquiv = inv→equiv 
   e  e .section , e .retract) 
   p  isEquivData (p .fst) (p .snd))
   _  refl)
   _  refl)

explode-Equiv : (A  B)  (Σ[ f  (A  B)] isEquiv f)
-- Exercise:
-- explode-Equiv = {!!}

In the Lecture 2-X we gave descriptions of PathPs in various types. The functions involved are also definitional inverses and so assemble into equivalences in a similar way.

-- mvrnote: is orienting these the other direction more natural?
×Path≃Path× : {x y : A × B} 
  (x .fst  y .fst) × (x .snd  y .snd)
   (x  y)
×Path≃Path× = inv→equiv ΣPathP→PathPΣ PathPΣ→ΣPathP  _  refl)  _  refl)

-- The same is true when everything is maximally dependent
ΣPath≃PathΣ : {A : I  Type }
                  {B : (i : I)  (a : A i)  Type ℓ'}
                  {x : Σ[ a  A i0 ] B i0 a}
                  {y : Σ[ a  A i1 ] B i1 a} 
  (Σ[ p  PathP A (x .fst) (y .fst) ]
    (PathP  i  B i (p i)) (x .snd) (y .snd)))
   (PathP  i  Σ[ a  A i ] B i a) x y)
ΣPath≃PathΣ = inv→equiv ΣPathP→PathPΣ PathPΣ→ΣPathP  _  refl)  _  refl)

We will not always be so lucky and have definitional inverses to our functions. For the following you will have to split into cases, like we did for the function isPositive-represents-Bool.

If the next equivalence doesn’t work doesn’t work, go back and check that the definitions of Bool→⊤⊎⊤ and ⊤⊎⊤→Bool you gave are actually inverses!

Bool≃⊤⊎⊤ : Bool  (  )
Bool≃⊤⊎⊤ = inv→equiv Bool→⊤⊎⊤ ⊤⊎⊤→Bool to-fro fro-to
  where
    to-fro : isSection Bool→⊤⊎⊤ ⊤⊎⊤→Bool
--  Exercise:
--  to-fro x = {!!}

    fro-to : isRetract Bool→⊤⊎⊤ ⊤⊎⊤→Bool
--  Exercise:
--  fro-to x = {!!}

The next few are similar.

ℤ≃ℕ⊎ℕ :   (  )
ℤ≃ℕ⊎ℕ = inv→equiv ℤ→ℕ⊎ℕ ℕ⊎ℕ→ℤ to-fro fro-to
  where
    to-fro : isSection ℤ→ℕ⊎ℕ ℕ⊎ℕ→ℤ
--  Exercise:
--  to-fro x = {!!}

    fro-to : isRetract ℤ→ℕ⊎ℕ ℕ⊎ℕ→ℤ
--  Exercise:
--  fro-to x = {!!}

⊎-ump-≃ : (A  C) × (B  C)  (A  B  C)
⊎-ump-≃ = inv→equiv ⊎-ump-to ⊎-ump-fro to-fro fro-to
  where
    to-fro : isSection ⊎-ump-to ⊎-ump-fro
--  Hint: You will need to case-split on the element of `A ⊎ B`, so
--  you can't use `refl` here immediately.
--  Exercise:
    to-fro f i (inl x) = f (inl x)

    fro-to : isRetract ⊎-ump-to ⊎-ump-fro
--  Exercise:
--  fro-to x = {!!}

∅×≃∅ : (A : Type )  ( × A)  
∅×≃∅ A = inv→equiv (∅×-to A) (∅×-fro A) to-fro fro-to
  where
    to-fro : isSection (∅×-to A) (∅×-fro A)
--  Exercise:
--  to-fro x = {!!}

    fro-to : isRetract (∅×-to A) (∅×-fro A)
--  Exercise:
--  fro-to x = {!!}

Torus≃S¹×S¹ : Torus   × 
Torus≃S¹×S¹ = inv→equiv Torus→S¹×S¹ S¹×S¹→Torus to-fro fro-to
  where
    to-fro : isSection Torus→S¹×S¹ S¹×S¹→Torus
--  Exercise:
--  to-fro c = {!!}

    fro-to : isRetract Torus→S¹×S¹ S¹×S¹→Torus
--  Exercise:
--  fro-to t = {!!}

All our type formers can be shown to respect equivalences, so that equivalent inputs give equivalent outputs. This is harder to show for some types than others, so we’ll have to come back to them, but the following few just involve rearranging the input data in simple ways. For these, it is best to not use the inv→equiv helper: the input equivalences have different sections and retracts, and so we should combine these to produce separate sections and retracts for the output equivalence.

-- mvrnote: make f/g vs e₁/e₂ consistent
×-map-≃ : (A  A')  (B  B')  (A × B)  (A' × B')
×-map-≃ {A = A} {A' = A'} {B = B} {B' = B'} f g = packEquiv to sec to-fro ret fro-to
  where
    to : A × B  A' × B'
    to = ×-map (f .map) (g .map)

    sec : A' × B'  A × B
--  Exercise:
--  sec (a' , b') = {!!}

    ret : A' × B'  A × B
--  Exercise:
--  ret (a' , b') = {!!}

    to-fro : isSection to sec
--  Exercise:
--  to-fro (a' , b') = {!!}

    fro-to : isRetract to ret
--  Exercise:
--  fro-to (a , b) = {!!}

→-map-≃ : (A  B)  (C  D)  (B  C)  (A  D)
→-map-≃ {A = A} {B = B} {C = C} {D = D} e₁ e₂ = packEquiv to sec to-fro ret fro-to
  where
    to : (B  C)  (A  D)
    to f a = e₂ .map (f (e₁ .map a))

    sec : (A  D)  (B  C)
--  Exercise:
--  sec g b = {!!}

    ret : (A  D)  (B  C)
--  Exercise:
--  ret g' b = {!!}

    to-fro : isSection to sec
--  Exercise:
--  to-fro g = {!!}

    fro-to : isRetract to ret
--  Exercise:
--  fro-to f = {!!}

Π-map-cod≃ : {P : A  Type } {Q : A  Type ℓ'}  ((x : A)  P x  Q x)  ((x : A)  P x)  ((x : A)  Q x)
Π-map-cod≃ {A = A} {P = P} {Q = Q} e = packEquiv to sec to-fro ret fro-to
  where
    to : ((x : A)  P x)  ((x : A)  Q x)
    to f x = e x .map (f x)

    sec : ((x : A)  Q x)  ((x : A)  P x)
--  Exercise:
--  sec g x = {!!}

    ret : ((x : A)  Q x)  ((x : A)  P x)
--  Exercise:
--  ret g' = {!!}

    to-fro : isSection to sec
--  Exercise:
--  to-fro g = {!!}

    fro-to : isRetract to ret
--  Exercise:
--  fro-to f = {!!}

⊎-map-≃ : (A  A')  (B  B')  (A  B)  (A'  B')
⊎-map-≃ {A = A} {A' = A'} {B = B} {B' = B'} f g = packEquiv to sec to-fro ret fro-to
  where
    to : A  B  A'  B'
    to = ⊎-map (f .map) (g .map)

    sec : A'  B'  A  B
--  Exercise:
--  sec t = {!!}

    ret : A'  B'  A  B
--  Exercise:
--  ret t = {!!}

    to-fro : isSection to sec
--  Exercise:
--  to-fro t = {!!}

    fro-to : isRetract to ret
--  Exercise:
--  fro-to t = {!!}

Equivalences do not necessarily go between different types. A type can be equivalent to itself in a non-trivial way!

not-≃ : Bool  Bool
not-≃ = inv→equiv not not to-fro fro-to
  where
    to-fro : isSection not not
--  Exercise:
--  to-fro x = {!!}

    fro-to : isSection not not
--  Exercise:
--  fro-to x = {!!}

sucℤ-≃ :   
-- Exercise:
-- sucℤ-≃ = inv→equiv sucℤ {!!} {!!} {!!}

Path Algebra

In the last lecture we saw what could be done with paths using only the fact that they are functions I → A. In this lecture, we’ll introduce some more axioms for the interval which will let us prove more.

So far, we have only used that the interval has endpoints i0 and i1. But the actual unit interval has a lot more structure than just its endpoints. We’ll add axioms to I that corresponds to this structure, and those operations on I will lead to new operations on Paths.

First, there is the function that reverses the interval. If is a path in the space from to , then is a path in from to — since and .

Cubical Agda has a primitive operation on elements of the interval: ~_ : I → I, which we think of as reversal, and by definition it holds that ~ i0 = i1 and ~ i1 = i0. You can test this by normalising ~ i0 via C-c C-n.

We can use this operation to reverse a path.

sym : x  y  y  x
sym p i = p (~ i)

And we can upgrade this principle to also apply to PathPs. We have to flip the path of types A too, so that the endpoints lie in the correct types.

symP : {A : I  Type }  {x : A i0}  {y : A i1}
   PathP A x y
   PathP  i  A (~ i)) y x
symP p j = p (~ j)

Now, there’s a evident question we can ask: what happens if we flip a path twice? Agda takes it as an axiom that ~ (~ i) = i, so the answer is that we get the same path again by definition.

symP-inv : (p : PathP _ x y)  symP (symP p)  p
symP-inv p = refl

And so symP is an equivalence.

symP-≃ : {A : I  Type }  {x : A i0}  {y : A i1}
   PathP A x y  PathP  i  A (~ i)) y x
symP-≃ = inv→equiv symP symP symP-inv symP-inv

To define some interesting Squares, we’ll axiomatize some more structure from the unit interval . Mathematically, the functions are quite useful for constructing homotopies: if is a path in , then is a homotopy between and the constant path at , because and . For similar reasons, is a homotopy between the constant path at and .

We will axiomatize these with two more in-built interval operations and , for and respectively. Agda computes the values of and when either side is known to be an endpoint i0 or i1.

Uncomment this block and try normalising the following expressions.

{-
_ : I
_ = {! i0 ∨ i0 !}

_ : I
_ = {! i0 ∨ i1 !}

_ : I
_ = {! i0 ∧ i0 !}

_ : I
_ = {! i0 ∧ i1 !}
-}

There are a few additional equalities which hold for and that Agda makes true for and . (You don’t have to memorise these.)

  • Top and Bottom:
  • Idempotence:
  • Commutativity:
  • Associativity:
  • Distributivity:
  • Symmetry:
  • The De Morgan Laws:

Aside: For a pen-and-paper exercise: Convince yourself that all of these axioms are true for the actual unit interval where ∨ = max, ∧ = min, and ~ i = 1 - i.

These laws make the interval I into an algebraic structure known as a De Morgan algebra. We saw a version of the “De Morgan laws” earlier for types when we proved DeMorgan-law-1, DeMorgan-law-2 and DeMorgan-law-3. Unlike for types, the algebra on the interval also satisfies the missing fourth law which we mentioned there.

Aside: De Morgan was a British mathematician and contemporary of Boole (from whom we get Boolean algebra and the name of the type Bool). He was the first to state the laws which have his name, coined the term “mathematical induction” and was the first to formally state the induction principle for natural numbers. De Morgan, like Boole, was concerned with turning logic into algebra.

We can use the De Morgan algebra structure and to build some squares that were unavailable to us before. The following two are called connections. The way we are drawing these, the arguments to Square are Square left right bottom top.

         p
     x — — — > y
     ^         ^                  ^    
refl |         | p              j |    
     |         |                  ∙ — >
     x — — — > x                    i  
        refl                  
connection∧ : (p : x  y)  Square refl p refl p
connection∧ p i j = p (i  j)
      refl
   y — — — > y
   ^         ^                    ^     
 p |         | refl             j |     
   |         |                    ∙ — > 
   x — — — > y                      i   
       p                     
connection∨ : (p : x  y)  Square p refl p refl
connection∨ p i j = p (i  j)

Below we have drawn some more squares for you to fill in as practice.

       p⁻¹
   y — — — > x
   ^         ^                    ^    
 p |         | refl             j |           
   |         |                    ∙ — >       
   x — — — > x                      i         
      refl                  
connectionEx1 : (p : x  y)  Square p refl refl (sym p)
-- Exercise:
-- connectionEx1 p i j = {!!}
        p
    x — — — > y
    ^         ^                   ^    
p⁻¹ |         | refl            j |    
    |         |                   ∙ — >
    y — — — > y                     i  
       refl                   
connectionEx2 : (p : x  y)  Square (sym p) refl refl p
-- Exercise:
-- connectionEx2 p i j = {!!}

As an immediate application of connections, we can show that the ℤ→ℤˢ and ℤˢ→ℤ maps we defined earlier are an equivalence. You will need to use a connection in the case for zeroˢ≡.

ℤ≃ℤˢ :   ℤˢ
ℤ≃ℤˢ = inv→equiv ℤ→ℤˢ ℤˢ→ℤ to-fro fro-to
  where
    to-fro : isSection ℤ→ℤˢ ℤˢ→ℤ
--  Exercise:
--  to-fro z = {!!}

    fro-to : isRetract ℤ→ℤˢ ℤˢ→ℤ
--  Exercise:
--  fro-to z = {!!}

Aside: When you reach the to-fro (zeroˢ≡ i) case, the goal should look like:

Goal: posˢ zero ≡ zeroˢ≡ i
———— Boundary (wanted) —————————————————————————————————————
i = i0 ⊢ λ i₁ → posˢ zero
i = i1 ⊢ zeroˢ≡
————————————————————————————————————————————————————————————
i : I

This is asking us for a path between paths, i.e. a square, but it is a little difficult to read in that form. If you accept an additional argument j (by adding it manually or pressing C-c C-c), the goal becomes

Goal: ℤˢ
———— Boundary (wanted) —————————————————————————————————————
j = i0 ⊢ posˢ zero
j = i1 ⊢ zeroˢ≡ i
i = i0 ⊢ posˢ zero
i = i1 ⊢ zeroˢ≡ j
————————————————————————————————————————————————————————————
j : I
i : I

which tells us exactly what square we need to construct.

References and Further Reading

https://arxiv.org/pdf/2408.11501