[question] Finding number of closed potential contours and area enclosed by them

Hello, I’m trying to calculate the number of closed contours in a potential as well the area enclosed by closed,

i.e. In this image below at certain energy level there are 4 closed contours. I’m trying to figure a way to calculate the number of closed contours (4 here) and the area enclosed by them, given the energy levels.

sample code for above (adopted from Pluto.jl notebook example),

using  GLMakie

len = 50
x = range(-1, 1, length=len)
y = range(-1, 1, length=len)
four_wells(x,y) = x^4 -  x^2 + y^4 - y^2
z = [four_wells(x[i], y[j]) for i in 1:len, j in 1:len]

# energy
height = -0.4

f = Figure(;resolution=(680,400))
ax1 = Axis3(f[1, 1])
ax2 = Axis(f[1, 2])

# Draw the contour in the 3D graph
plane = [height for i in 1:length(x), j in 1:length(y)];

# Drawing the function and the contour on the 3G graph
surface!(ax1, x, y, z, colormap=:viridis)
surface!(ax1, x, y, plane; transparency=true, colormap=[:white,:white])

# Drawing the function and the countour on the heatmap
hm = contourf!(ax2, x, y, z)
Colorbar(f[:, 3], hm; vertical=true)
contour!(ax2, x, y, z; linewidth=3, levels=[height], color=:white)
f
Pluto notebook file for the same
### A Pluto.jl notebook ###
# v0.19.35

using Markdown
using InteractiveUtils

# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error).
macro bind(def, element)
    quote
        local iv = try Base.loaded_modules[Base.PkgId(Base.UUID("6e696c72-6542-2067-7265-42206c756150"), "AbstractPlutoDingetjes")].Bonds.initial_value catch; b -> missing; end
        local el = $(esc(element))
        global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : iv(el)
        el
    end
end

# ╔═╡ ca18be6d-8a9b-4c53-bd2e-553ea01ebb13
using PlutoUI, GLMakie

# ╔═╡ 503aa0a0-ece8-4acc-b437-3119055d22bc
k = @bind height PlutoUI.Slider(-1:0.1:1; default=0, show_value=true)

# ╔═╡ 2dab2fcd-77bf-4651-8223-6854863f425a
md"""
# Appendix
"""

# ╔═╡ 58d43864-9a74-42da-96d5-afb59f22cd0f
begin
	len = 50
	x = range(-1, 1, length=len)
	y = range(-1, 1, length=len)
	four_wells(x,y) = x^4 -  x^2 + y^4 - y^2
	z = [four_wells(x[i], y[j]) for i in 1:len, j in 1:len]
end

# ╔═╡ e39373bd-8e12-469f-bd66-2d005ff6cff2
begin
	# Setup of the figure
    f = Figure(;resolution=(680,400))
    ax1 = Axis3(f[1, 1])
    ax2 = Axis(f[1, 2])

	# Draw the contour in the 3D graph
	plane = [height for i in 1:length(x), j in 1:length(y)];
	
	# Drawing the function and the contour on the 3G graph
	surface!(ax1, x, y, z, colormap=:viridis)
	surface!(ax1, x, y, plane; transparency=true, colormap=[:white,:white])

	# Drawing the function and the countour on the heatmap
	hm = contourf!(ax2, x, y, z)
	Colorbar(f[:, 3], hm; vertical=true)
    contour!(ax2, x, y, z; linewidth=3, levels=[height], color=:white)
end;

# ╔═╡ 9c17c0d8-e4cb-41c0-b180-b18a3c87e265
# Displaying the function
f

# ╔═╡ Cell order:
# ╠═503aa0a0-ece8-4acc-b437-3119055d22bc
# ╟─9c17c0d8-e4cb-41c0-b180-b18a3c87e265
# ╠═e39373bd-8e12-469f-bd66-2d005ff6cff2
# ╟─2dab2fcd-77bf-4651-8223-6854863f425a
# ╠═ca18be6d-8a9b-4c53-bd2e-553ea01ebb13
# ╠═58d43864-9a74-42da-96d5-afb59f22cd0f

There’s a python implementation on stackoverflow, any pointers on which packages to look on Julia will be more helpful.

I’m not familiar with the internals of Makie’s contour code, so I can’t offer a solution as elegant as the python equivalent. Nevertheless, here is an idea for how to do a the calculation using other Julia tools:

  • To obtain a representation of the contour, MDBM.jl will be able to sample the iso-surface of the potential
  • The list of points given by MDBM.jl will not distinguish the number of closed contours, so you will have to cluster the points into disconnected contours. It should be fairly easy to write a function for this if there aren’t any packages available.
  • With the list of points in each contour, you can sort these points in a counterclockwise fashion and calculate their area, e.g. using the shoelace formula as in the python example

I hope these ideas help

1 Like

Funny, this seems the fill pits steps in DEMs. Found this package WhereTheWaterFlows that has to solve it as an intermediate step.

1 Like

Another implementation in python, shapely · PyPI.

from shapely.geometry import Polygon
ct = [ (0,0), (0,1), (1,1), (1,0) ]
polygon = Polygon(ct)
>>> polygon.area
1.0

The DEM-filling in WhereTheWaterflows will fill the local minima completely. In above example it would fill those dips with “water” until their shores touch. So, probably not the right thing. But it should be possible to extract the contours from the plotting datastructure somehow… Ah, Makie calls into GitHub - JuliaGeometry/Contour.jl: Calculating contour curves for 2D scalar fields in Julia .

1 Like

Yes, but if we then subtract the filled and non filled grids the zero contour of that difference grid will be what is seemed here.

In fact I’m trying to get that point. So, what I understood from DEM-filling is, if I starting filling water in a local minima and it fills gradually till it reaching the brim and then a sudden increase in volume.

Will I be able to plot volume filled with respect to height capturing the above behaviour?

For the area you could use ImplicitIntegration.jl:

using GLMakie
using ImplicitIntegration

len = 50
x = range(-1, 1; length = len)
y = range(-1, 1; length = len)
four_wells(x, y) = x^4 - x^2 + y^4 - y^2
z = [four_wells(x[i], y[j]) for i in 1:len, j in 1:len]

# energy
height = -0.4

f = Figure(; resolution = (680, 400))
ax1 = Axis3(f[1, 1])
ax2 = Axis(f[1, 2])

# Draw the contour in the 3D graph
plane = [height for i in 1:length(x), j in 1:length(y)];

# Drawing the function and the contour on the 3G graph
surface!(ax1, x, y, z; colormap = :viridis)
surface!(ax1, x, y, plane; transparency = true, colormap = [:white, :white])

# Drawing the function and the countour on the heatmap
hm = contourf!(ax2, x, y, z)
Colorbar(f[:, 3], hm; vertical = true)
contour!(ax2, x, y, z; linewidth = 3, levels = [height], color = :white)

# compute a quadrature for the implicit region ϕ<0
ϕ = (x) -> four_wells(x[1],x[2]) - height              # level-set
quad, logger = quadgen(ϕ, (-1, -1), (1, 1); order = 3)
println("Estimated area: ", integrate(x -> 1, quad))
# visualize the quadrature nodes
scatter!(ax2, quad.coords; markersize = 6)

f

But I don’t think you can easily obtain the connected components using ImplicitIntegration.jl

2 Likes

Here’s how you can get the line coordinates with Contours.jl:

using Contours


len = 50
x = range(-1, 1; length = len)
y = range(-1, 1; length = len)
four_wells(x, y) = x^4 - x^2 + y^4 - y^2
z = [four_wells(x[i], y[j]) for i in 1:len, j in 1:len]

# energy
height = -0.4

cls = contours(x, y, z, [height]) # compute the contours at manually specified levels
level = only(levels(cls)) # extract the only result (since we only asked for one level)

points = [l.vertices for l in lines(level)] # ::Vector{Vector{Tuple{Float64, Float64}}}

Now that we have the lines, we need to check that they’re closed. The nature of Contour.jl’s algorithm makes this simple - we only need to know whether the first and last points are the same.

closed_ring_points = filter(points) do pointvec
    pointvec[begin] == pointvec[end] # you can also make this approximate, if you want
end

Now that you have polygon-like constructs, it’s easy enough to solve for its area. There are multiple solutions, I’m using GeoInterface.jl and GeometryOps.jl here.

import GeoInterface as GI, GeometryOps as GO
# construct geometry from vectors of points
rings = GI.LinearRing.(closed_ring_points)
polygons = [GI.Polygon([ring]) for ring in rings]

areas = GO.area.(polygons) # 4x ~0.1694

# if you want to be efficient / nonallocating
total_area = sum(GO.area, polygons) # 0.6775172177537117

Hope that helps! If you are worried about intersecting polygons, you can also use GeometryOps.jl to fix that:

mp = GI.MultiPolygon(polygons)

fixed_mp = GO.fix(mp; corrections = [GO.UnionIntersectingPolygons()])
2 Likes

Mind linking Contours.jl repo, the link you provided isn’t pointing to the package.

Edit: It seems discourse auto searches the package, again in my local environment too I can find only Contour not Contours and it doesn’t have the function contours.

This is correct github link of Contour.jl and this is link of Contour.jl documentation. See contours() function in this image inside levels().

1 Like

but that doesnt provide the function contours

julia> Contour.
ContourCollection   ContourLevel        Curve2              E
EN                  ES                  EW                  N
NE                  NESW                NS                  NW
NWSE                S                   SE                  SN
SW                  W                   WE                  WN
WS                  _get_case           advance_edge        chase!
contour             contourlevels       contours            coordinates
edge_LUT            eval                get_first_crossing  get_level_cells
get_next_edge!      include             interpolate         level
levels              lines               next_edge           next_map
trace_contour       vertices

edit 1,

julia> @which contours(x, y, z, [height])
contours(x, y, z, levels; VT)
     @ Contour ~/.julia/packages/Contour/vefVR/src/Contour.jl:94

seems to be Contour.jl/src/Contour.jl at master · JuliaGeometry/Contour.jl · GitHub

I have manually included the above Contour.jl and interpolate.jl, seems to work now but I wonder why using Contour is not providing the function.

Ah yeah my bad, it should have been Contour.jl.

Can you confirm the version of the package? It should be the latest.

1 Like

Still interested on this? With GMT.jl master we can now do.

G = peaks(N=128);
G2 = fillsinks(G);
viz(G2, shade=true)

Can you elaborate a bit on how G is defined and how to get area by using fillsinks.

I couldn’t find that function on the documentation page.

The HTML documentation has not yet been generated but the online dons do exist. Just do the normal ? fillsinks
This function is meant to fill the sinks, not to measure the area. But we can that as well. There is an option to also save the contours (which are not exactly contours, see the docs about the 256 levels) in a global variable.

G = peaks(N=128);
G2 = fillsinks(G, saco=true);
D = get(GMT.SACO[1], "saco", nothing);

gmtspatial(D, area=true)
BoundingBox: [-1.831659459383318, 0.31258763887390784, -1.923463287251611, 0.2138927839499514, 0.3258726517453034, 4.3948167896335795]

3×3 GMTdataset{Float64, 2}
 Row │ centroid_x  centroid_y      area
─────┼──────────────────────────────────
   1 │   0.312588    0.213893  0.325873
   2 │  -1.83166     0.201386  3.74307
   3 │   0.293074   -1.92346   4.39482

G is just a simple demo grid made with the peaks function. Note that for you to use this you must convert your array into a GMTgrid type. The function mat2grid will help on that.

1 Like

Found a cool guide about visualization of contour area using GLMakie.

1 Like