Skip to content

Adding nanoprisms model (pure python)#699

Open
sara-mokhtari wants to merge 10 commits intomasterfrom
adding_nanoprisms_model
Open

Adding nanoprisms model (pure python)#699
sara-mokhtari wants to merge 10 commits intomasterfrom
adding_nanoprisms_model

Conversation

@sara-mokhtari
Copy link
Copy Markdown
Contributor

Description

Adding a pure python model for nanoprisms of different cross sections with orientation averaging using Fibonacci quadrature.

Fixes #685

How Has This Been Tested?

Unit tests worked.
This model was compared to an another numerical calculation : for a gold nanoprism with a pentagonal cross section, agreement was found with the Debye equation (Debye calculator: https://github.com/FrederikLizakJohansen/DebyeCalculator). The Debye equation is based on atomic positions while SasView model is based on analytical expressions.
The model was also used to fit an experimental data (AuAg nanoprism of pentagonal cross section D = 30 nm, L = 117 nm).

Since it's a python model, there is still the issue: "the support orientational dispersion in pure python model" ( #695).

More tests should be made on small q. Indeed, like the previous model (octahedron_truncated model), we encouter issues when it comes to small q (q<10^-6 Angs). More precise mathematical should be used (see PR #694 comments).

Note : the fibonacci quadrature code (sasmodels/quadratures/fibonacci.py) was added to a new repository called "quadratures" because it could be also useful for other models.

To do:

  • take care of the orientational dispersion for pure python model
  • add numba acceleration (tests have to be made first)
  • take care of the singularities

Review Checklist:

[if using the editor, use [x] in place of [ ] to check a box]

Documentation (check at least one)

  • There is nothing that needs documenting
  • Documentation changes are in this PR
  • There is an issue open for the documentation (link?)

Installers

  • There is a chance this will affect the installers, if so
    • Windows installer (GH artifact) has been tested (installed and worked)
    • MacOSX installer (GH artifact) has been tested (installed and worked)
    • Wheels installer (GH artifact) has been tested (installed and worked)

Licensing (untick if necessary)

  • The introduced changes comply with SasView license (BSD 3-Clause)

@butlerpd
Copy link
Copy Markdown
Member

butlerpd commented Mar 9, 2026

I have not had the chance to look at this but note that you created a new directory at sasmodels.quadratures. I thought all our current quadratures reside in sasmodels.models.lib It may be that we want to move quadruture code into its own directory but that should be a discussion I guess? Also not sure the higher level (under sasmodels rather than sasmodels.models is the right answer?

@pkienzle
Copy link
Copy Markdown
Contributor

pkienzle commented Mar 9, 2026

I don't remember the details, but I found that I needed to put the 2-Yukawa support libraries into sasmodels.TwoYukawa rather than sasmodels.models.TwoYukawa. Similarly, the additional quadrature support code cannot be in the sasmodels.models namespace.

We may want to create a sasmodels.lib package to contain the support libraries for pure python models. Currently these are in sasmodels.special, the sasmodels.TwoYukawa package, and the new sasmodels.quadrature module.

Note: PEP8 says package names should be snake_case. Somehow both the author (me) and the reviewers messed up with TwoYukawa. We can fix that if we move support libraries to lib.

I'll create a ticket for this.

I wasn't using the weights because of this line, not sure why i put it ?
Copy link
Copy Markdown
Contributor

@pkienzle pkienzle left a comment

Choose a reason for hiding this comment

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

You could convert a lot of the steps to use numpy vector operators. I don't think numba will give a big speedup so I didn't try it. Torch would allow you to compute on the GPU, and that would probably be faster.

I think you are only running this with a small number of vertices. If the number of vertices were significantly larger than the number of q points then you may want to use vector operations across the vertices and loop over the q values. (This is unlikely given that q points for all |q| shells in the Iq calculation are sent is as one big vector.)

a loop, all in the plane
"""
qmodulus2 = q[0]**2+q[1]**2
qmodulus2 = np.asanyarray(qmodulus2) # conversion to array
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Already an array otherwise the previous line wouldn't work.

vertices : list
List of the vertices of the regular polygon.
"""
vertices = [[radius*np.cos(2*step*np.pi/number_of_sides), radius*np.sin(2*step*np.pi/number_of_sides)] for step in range(0,number_of_sides)]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

with numpy, returns points[2 x n]:

theta = np.linspace(0, 2 * np.pi, number_of_sides, endpoint=False)
return radius*np.vstack((np.cos(theta), np.sin(theta))

for j in range(2):
coordinates.append((extended_vertices[i+1][j]+extended_vertices[i][j])/2)
edgecenter.append(coordinates)
return edgecenter
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

with numpy [2 x n] => [2 x n]:

return (vertices + np.roll(vertices, -1, axis=1))/2

for j in range(2):
coordinates.append((extended_vertices[i+1][j]-extended_vertices[i][j])/2)
halfedge.append(coordinates)
return halfedge
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

numpy operation:

return (np.roll(vertices, -1, axis=1) - vertices)/2

Form factor amplitude of the nanoprism at the specific three dimensional q.
"""

qab = [qa,qb]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Assume qa, qb, qc are vectors. We could use tensor operations instead, performing the dot product with np.einsum('ijk,kl->ijl') but it is easy enough to reshape our inputs to flattened arrays inside Iq, so no need to bother.

qab = np.vstack((qa, qb)).T

We could save some memory by having a helper function that takes a q matrix rather qa, qb, qc separately. At 500 fibonacci points it will not be an issue.

qb = q[:, np.newaxis] * q_unit[:, 1][np.newaxis, :]
qc = q[:, np.newaxis] * q_unit[:, 2][np.newaxis, :]
# Compute intensity
intensity = Iqabc(qa, qb, qc, nsides, Rave, L) # shape (nq, npoints)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Convert qunit x radius in a vectors to compute apoints for all shells at once, then convert back to a matrix to sum across shells.

intensity = Iqabc(qa.ravel(), qb.ravel(), qc.ravel(), nsides, Rave, L).reshape(qa.shape)

# Compute intensity
intensity = Iqabc(qa, qb, qc, nsides, Rave, L) # shape (nq, npoints)
# Uniform average over the sphere
integral = np.sum(w[np.newaxis, :] * intensity, axis=1)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fibonacci is equal-weighted, so no need for w. If it weren't equal-weighted, then you could use integral = intensity @ w to produce the weighted sum in place rather than first forming the matrix Iqij*wi then summing the rows.

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.

Nanoprism model

7 participants