Skip to content

Conversation

AreWeDreaming
Copy link
Contributor

This adds to geometry routines that compute intersections between a ray and a torus with a rectangular cross section.
Still work in progress because I need to remove ray_rect_intersection since that is untested LLM code that I don't need.

@AreWeDreaming AreWeDreaming marked this pull request as ready for review June 17, 2025 18:38
@AreWeDreaming
Copy link
Contributor Author

Should be ready to go. This has extensive tests that should cover almost every line.

Copy link
Member

@bclyons12 bclyons12 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a bunch of suggestions to Julia-fy the code, but does the existing toroidal_intersection() do what you want?

src/geometry.jl Outdated
t_interval_z = [0.0 Inf]
end
else
t_interval_z = sort![(z_min - z0) / dz, (z_max - z0) / dz]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sort![...] can't be right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
t_interval_z = sort![(z_min - z0) / dz, (z_max - z0) / dz]
t_interval_z = sort([(z_min - z0) / dz, (z_max - z0) / dz])

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sort!([(z_min - z0) / dz, (z_max - z0) / dz]) at the every least, but sort(@SVector[(z_min - z0) / dz, (z_max - z0) / dz]) would be better.

src/geometry.jl Outdated
crossing = zeros(Float64, 4) # +1 -> into domain
# -1 -> out of
# 0 -> no crossing
into_domain = [1.0, -1.0]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these small, fixed-sized arrays should either be Tuples or SVector/MVector from StaticArrays.jl. This will avoid unnecessary allocations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
into_domain = [1.0, -1.0]
into_domain = Tuple([1.0, 1.0])

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

into_domain = (1.0, 1.0)

src/geometry.jl Outdated
if (r0 > r_min && r0 < r_max)
return [[0.0 Inf];]
else
return zeros(Float64, 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want an empty array? Float64[] is more idiomatic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return zeros(Float64, 0)
return Float64[]

src/geometry.jl Outdated
if dx == 0 && dy == 0
# Handle vertical ray
if (r0 > r_min && r0 < r_max)
return [[0.0 Inf];]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[0.0 Inf] will give you a 1x2 Matrix

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return [[0.0 Inf];]
return [0.0 Inf]

src/geometry.jl Outdated
t_crossings[2*(i_ref-1) + 1] = t1 >= 0 ? t1/ (dx^2 + dy^2) : Inf
t_crossings[2*(i_ref-1) + 2] = t2 >= 0 ? t2/ (dx^2 + dy^2) : Inf
crossing[2*(i_ref-1) + 1] = t1 >= 0 ? into_domain[i_ref]*sign(x0*dx + dx^2*t1 + y0 * dy + dy^2*t1) : 0.0
crossing[2*(i_ref-1) + 2] = t2 >= 0 ? into_domain[i_ref]*sign(x0*dx + dx^2*t2 + y0 * dy + dy^2*t2) : 0.0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this ever becomes computationally intensive, there are plenty of redundant FLOPs here that could be avoided.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is run once per beam so it should not come up too often and cost negligible time compared to the actual raytracing.

src/geometry.jl Outdated
append!(crossing, 1.0)
end
i_sort = sortperm(t_crossings)
intervals = zeros(Float64, 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Float64[]

src/geometry.jl Outdated
end
end
if length(intervals) % 2 != 0
throw(ErrorException("Somehow only found odd number of intersections which is impossible."))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure grazing intersections are properly handled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my specific use-case grazing intersection can raise an error. A beam that hits the rectangular flux matrix parallel to one of its boundaries indicates an error in how the beam is set up.

src/geometry.jl Outdated

function solve_r_intersect(x0::T, y0::T, dx::T, dy::T, r_min::T, r_max::T) where {T<:Real}
r0 = sqrt(x0^2 + y0^2)
t_crossings = zeros(Float64, 4)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
t_crossings = zeros(Float64, 4)
t_crossings = Float64[]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changed the size.

src/geometry.jl Outdated
append!(crossing, 1.0)
end
i_sort = sortperm(t_crossings)
intervals = zeros(Float64, 0)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
intervals = zeros(Float64, 0)
intervals = Float64[]

@AreWeDreaming
Copy link
Contributor Author

As far as I can tell toroidal_intersection in particles.jl is iterative. This is analytic and therefore should be more accurate and computationally more efficient.

@bclyons12
Copy link
Member

toroidal_intersection() is analytic. It iterates if you give it multiple segments to try to intersect with. It was originally made for a ballistic trajectories of particles hitting a wall, but it's should work here.

"""
toroidal_intersection(r1::Real, z1::Real, r2::Real, z2::Real, px::Real, py::Real, pz::Real, vx::Real, vy::Real, vz::Real, v2::Real, vz2::Real)
Compute the time of intersection between a moving particle and a toroidal surface defined by two points `(r1, z1)` and `(r2, z2)`.
Returns the smallest positive time at which the particle intersects the surface segment. Returns `NaN` if no valid intersection occurs.
- `r1`, `z1`: Coordinates of the first endpoint of the toroidal surface segment.
- `r2`, `z2`: Coordinates of the second endpoint of the toroidal surface segment.
- `px`, `py`, `pz`: Current position of the particle in Cartesian coordinates.
- `vx`, `vy`, `vz`: Velocity components of the particle.
- `v2`: Squared radial velocity component, `vx^2 + vy^2`.
- `vz2`: Squared z-velocity component, `vz^2`.
"""
function toroidal_intersection(r1::Real, z1::Real, r2::Real, z2::Real, px::Real, py::Real, pz::Real, vx::Real, vy::Real, vz::Real, v2::Real, vz2::Real)
t = Inf
if isapprox(z1, z2)
# a horizontal segment: time for how long it takes to get to this z, then check if r is between segment bounds
ti = (vz == 0.0) ? -Inf : (z1 - pz) / vz
if ti >= 0
ri = sqrt((px + vx * ti)^2 + (py + vy * ti)^2)
if ri >= r1 && ri <= r2
t = ti
end
end
else
# parameterize parabola intersection with a line
m = (z2 - z1) / (r2 - r1)
z0 = z1 - m * r1
m2 = m^2
a = 0.0
b = 0.0
c = 0.0
if isfinite(m)
z_z0 = pz - z0
a = m2 * v2 - vz2
b = 2 * (m2 * (vx * px + vy * py) - z_z0 * vz)
c = m2 * (px^2 + py^2) - z_z0^2
else
a = v2
b = 2 * (vx * px + vy * py)
c = px^2 + py^2 - r1^2
end
t1 = -Inf
t2 = -Inf
if a == 0
b != 0 && (t2 = -c / b)
else
nb_2a = -b / (2a)
b2a_ca = (b^2 - 4 * a * c) / (4 * a^2)
if b2a_ca > 0
t_sq = sqrt(b2a_ca)
t1 = nb_2a - t_sq
t2 = nb_2a + t_sq
elseif b2a_ca 0.0
t2 = nb_2a
end
end
# check if intersection points are at positive time and fall between segment
t = Inf
if t1 >= 0
zi = vz * t1 + pz
if zi >= z1 && zi <= z2
t = t1
end
end
if !isfinite(t) && t2 >= 0
zi = vz * t2 + pz
if zi >= z1 && zi <= z2
t = t2
end
end
end
if t == Inf
t = NaN
end
return t
end

@AreWeDreaming
Copy link
Contributor Author

@bclyons12 you were right, the existing routines do what I need to do. I am not sure why I didn't want to use them initially. I almost everything. Now all this PR does is add another dispatch to the toroidal_intersection method such that it accepts p and v as vectors. I also rewrote the test I contribute here so that it tests the toroidal_intersection method instead.

@AreWeDreaming
Copy link
Contributor Author

(I also tested the regression test in TorJ which now uses toroidal_intersection and that has passed as well.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants