Skip to content
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
11ba48c
Add truncated octahedron (python+C) model
sara-mokhtari Nov 13, 2025
71974fb
Add truncated octahedron (python+C) model updated (image added)
sara-mokhtari Nov 13, 2025
e08dac9
Add truncated octahedron (python+C) model updated (PEP8 convention)
sara-mokhtari Nov 13, 2025
4c49098
Add truncated octahedron (python+C) model updated again (PEP8)
sara-mokhtari Nov 13, 2025
53a2fd5
Add truncated octahedron (python+C) model updated (test failed)
sara-mokhtari Nov 14, 2025
b0c14de
Add truncated octahedron (python+C) model updated (fixing tests)
sara-mokhtari Nov 14, 2025
fb79959
Add truncated octahedron (python+C) model updated (trying to remove t…
sara-mokhtari Nov 14, 2025
fa35edd
Add truncated octahedron (python+C) model updated (NaN issue and test)
sara-mokhtari Nov 14, 2025
8653749
Add truncated octahedron (python+C) model updated (rst issue)
sara-mokhtari Nov 14, 2025
eec5414
Add truncated octahedron (python+C) model updated (rst issue and ruff)
sara-mokhtari Nov 14, 2025
9f29bda
Add truncated octahedron (python+C) model updated
sara-mokhtari Nov 14, 2025
81c6d17
Add truncated octahedron (python+C) model updated
sara-mokhtari Nov 14, 2025
f01a7c1
Add truncated octahedron (python+C) model updated (rst issue and ruff)
sara-mokhtari Nov 14, 2025
54ed97c
Add truncated octahedron (python+C) model updated (rst issue and ruff)
sara-mokhtari Nov 14, 2025
b132d27
Add truncated octahedron (python+C) model updated
sara-mokhtari Nov 14, 2025
6c31722
Add truncated octahedron (python+C) model updated
sara-mokhtari Nov 14, 2025
fcfe74e
Add truncated octahedron (python+C) model updated
sara-mokhtari Nov 14, 2025
6807855
Add truncated octahedron (python+C) model updated
sara-mokhtari Nov 14, 2025
557209d
Add truncated octahedron (python+C) model updated
sara-mokhtari Nov 14, 2025
76adf42
Update octahedron_truncated.py
wpotrzebowski Nov 15, 2025
f11bc68
Update octahedron_truncated.py
wpotrzebowski Nov 15, 2025
969da09
Update octahedron_truncated.py
wpotrzebowski Nov 15, 2025
20321f6
Update octahedron_truncated.py
wpotrzebowski Nov 15, 2025
b871ab1
changed documentation
sara-mokhtari Nov 15, 2025
1834d80
changed documentation
sara-mokhtari Nov 15, 2025
679262c
add new category by changing category in octohedron model
Nov 15, 2025
5cac0ab
update of octa-truncated.png image file
marimperorclerc Nov 21, 2025
ff9442c
Merge branch 'master' into adding_trOh_model_pythonC
marimperorclerc Nov 21, 2025
6bf1b94
Changing the documentation in the python code
sara-mokhtari Dec 1, 2025
12a2efb
Updating the c code
sara-mokhtari Dec 1, 2025
47b02ba
Changing octahedron_truncated c and py code: changing documentation
sara-mokhtari Dec 3, 2025
42eea23
Changing octahedron_truncated.c
sara-mokhtari Dec 3, 2025
de8df47
Handle NaN and infinity in octahedron calculations
sara-mokhtari Dec 4, 2025
7b6bc2c
Fix test values (sig)
sara-mokhtari Dec 10, 2025
0666f47
Fix LaTeX formatting in octahedron_truncated.py
sara-mokhtari Dec 10, 2025
63e5f69
Update octahedron_truncated.py
marimperorclerc Dec 11, 2025
dd32c89
latex doc (math expressions) in octahedron_truncated.py
sara-mokhtari Dec 12, 2025
5afaff5
Fix LaTeX formatting in octahedron_truncated.py (again)
sara-mokhtari Dec 12, 2025
7a444c7
latex doc last fix
sara-mokhtari Dec 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added sasmodels/models/img/octa-truncated.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
249 changes: 249 additions & 0 deletions sasmodels/models/octahedron_truncated.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
#include <math.h>
#include <stdio.h>

//truncated octahedron volume
// NOTE: needs to be called form_volume() for a shape category
static double
form_volume(double length_a, double b2a_ratio, double c2a_ratio, double t)
{
// length_a is the half height along the a axis of the octahedron without truncature
// length_b is the half height along the b axis of the octahedron without truncature
// length_c is the half height along the c axis of the octahedron without truncature
// b2a_ratio is length_b divided by Length_a
// c2a_ratio is Length_c divided by Length_a
// t varies from 0.5 (cuboctahedron) to 1 (octahedron)
return (4./3.) * cube(length_a) * b2a_ratio * c2a_ratio *(1.-3*cube(1.-t));
}

// remark: Iq() is generally not used because have_Fq is set to True in the Python file
static double
Iq(double q,
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't need Iq when you have Fq

Copy link
Contributor

Choose a reason for hiding this comment

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

Agree with this, but we keep here the initial code as Iq() is called most of the time.

Copy link
Contributor

Choose a reason for hiding this comment

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

Iq is not use if Fq is present.

The code in generate.py uses the have_Fq attribute to generate either a call to Fq or to Iq:

if model_info.have_Fq:
pars = ",".join(["_q", "&_F1", "&_F2",] + model_refs)
call_iq = "#define CALL_FQ(_q, _F1, _F2, _v) Fq(%s)" % pars
clear_iq = "#undef CALL_FQ"
else:
pars = ",".join(["_q"] + model_refs)
call_iq = "#define CALL_IQ(_q, _v) Iq(%s)" % pars
clear_iq = "#undef CALL_IQ"

The code in kernel_iq.c checks if call Fq or call Iq is defined:

#if defined(CALL_FQ) // COMPUTE_F1_F2 is true
// unoriented 1D returning <F> and <F^2>
// Note that F1 and F2 are returned from CALL_FQ by reference, and the
// user of the CALL_KERNEL macro below is assuming that F1 and F2 are defined.
double qk;
double F1, F2;
#define FETCH_Q() do { qk = q[q_index]; } while (0)
#define BUILD_ROTATION() do {} while(0)
#define APPLY_ROTATION() do {} while(0)
#define CALL_KERNEL() CALL_FQ(qk,F1,F2,local_values.table)
#elif defined(CALL_FQ_A)
// unoriented 2D return <F> and <F^2>
// Note that the CALL_FQ_A macro is computing _F1_slot and _F2_slot by
// reference then returning _F2_slot. We are calling them _F1_slot and
// _F2_slot here so they don't conflict with _F1 and _F2 in the macro
// expansion, or with the use of F2 = CALL_KERNEL() when it is used below.
double qx, qy;
double _F1_slot, _F2_slot;
#define FETCH_Q() do { qx = q[2*q_index]; qy = q[2*q_index+1]; } while (0)
#define BUILD_ROTATION() do {} while(0)
#define APPLY_ROTATION() do {} while(0)
#define CALL_KERNEL() CALL_FQ_A(sqrt(qx*qx+qy*qy),_F1_slot,_F2_slot,local_values.table)
#elif defined(CALL_IQ)
// unoriented 1D return <F^2>
double qk;
#define FETCH_Q() do { qk = q[q_index]; } while (0)
#define BUILD_ROTATION() do {} while(0)
#define APPLY_ROTATION() do {} while(0)
#define CALL_KERNEL() CALL_IQ(qk,local_values.table)
#elif defined(CALL_IQ_A)
// unoriented 2D
double qx, qy;
#define FETCH_Q() do { qx = q[2*q_index]; qy = q[2*q_index+1]; } while (0)
#define BUILD_ROTATION() do {} while(0)
#define APPLY_ROTATION() do {} while(0)
#define CALL_KERNEL() CALL_IQ_A(sqrt(qx*qx+qy*qy), local_values.table)
#elif defined(CALL_IQ_AC)
// oriented symmetric 2D
double qx, qy;
#define FETCH_Q() do { qx = q[2*q_index]; qy = q[2*q_index+1]; } while (0)
double qa, qc;
QACRotation rotation;
// theta, phi, dtheta, dphi are defined below in projection to avoid repeated code.
#define BUILD_ROTATION() qac_rotation(&rotation, theta, phi, dtheta, dphi);
#define APPLY_ROTATION() qac_apply(&rotation, qx, qy, &qa, &qc)
#define CALL_KERNEL() CALL_IQ_AC(qa, qc, local_values.table)
#elif defined(CALL_IQ_ABC)
// oriented asymmetric 2D
double qx, qy;
#define FETCH_Q() do { qx = q[2*q_index]; qy = q[2*q_index+1]; } while (0)
double qa, qb, qc;
QABCRotation rotation;
// theta, phi, dtheta, dphi are defined below in projection to avoid repeated code.
// psi and dpsi are only for IQ_ABC, so they are processed here.
const double psi = values[details->theta_par+4];
local_values.table.psi = 0.;
#define BUILD_ROTATION() qabc_rotation(&rotation, theta, phi, psi, dtheta, dphi, local_values.table.psi)
#define APPLY_ROTATION() qabc_apply(&rotation, qx, qy, &qa, &qb, &qc)
#define CALL_KERNEL() CALL_IQ_ABC(qa, qb, qc, local_values.table)
#elif defined(CALL_IQ_XY)
// direct call to qx,qy calculator
double qx, qy;
#define FETCH_Q() do { qx = q[2*q_index]; qy = q[2*q_index+1]; } while (0)
#define BUILD_ROTATION() do {} while(0)
#define APPLY_ROTATION() do {} while(0)
#define CALL_KERNEL() CALL_IQ_XY(qx, qy, local_values.table)
#endif

Copy link
Contributor

Choose a reason for hiding this comment

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

OK ! Thank you for this.

double sld,
double solvent_sld,
double length_a,
double b2a_ratio,
double c2a_ratio,
double t)
{
const double length_b = length_a * b2a_ratio;
const double length_c = length_a * c2a_ratio;


//Integration limits to use in Gaussian quadrature
const double v1a = 0.0;
const double v1b = M_PI_2; //theta integration limits
const double v2a = 0.0;
const double v2b = M_PI_2; //phi integration limits

double outer_sum = 0.0;
for(int i=0; i<GAUSS_N; i++) {
const double theta = 0.5 * ( GAUSS_Z[i]*(v1b-v1a) + v1a + v1b );
double sin_theta, cos_theta;
SINCOS(theta, sin_theta, cos_theta);

double inner_sum = 0.0;
for(int j=0; j<GAUSS_N; j++) {
double phi = 0.5 * ( GAUSS_Z[j]*(v2b-v2a) + v2a + v2b );
double sin_phi, cos_phi;
SINCOS(phi, sin_phi, cos_phi);

//HERE: Octahedron formula
// q is the modulus of the scattering vector in [A-1]
// NOTE: capital QX QY QZ are the three components in [A-1] of the scattering vector
// NOTE: qx qy qz are rescaled components (no unit) for computing AA, BB and CC terms
const double Qx = q * sin_theta * cos_phi;
const double Qy = q * sin_theta * sin_phi;
const double Qz = q * cos_theta;
const double qx = Qx * length_a;
const double qy = Qy * length_b;
const double qz = Qz * length_c;

const double AA = 1./((qy*qy-qz*qz)*(qy*qy-qx*qx))*((qy-qx)*sin(qy*(1.-t)-qx*t)+(qy+qx)*sin(qy*(1.-t)+qx*t))+
Copy link
Contributor

Choose a reason for hiding this comment

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

(qyqy - qzqz) probably needs to be taken care of. Otherwise we can end up in NaN/Inf scenario

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, we have this issue on how to handle singularities numerically.
To be discussed furthermore, in particular for the pure python model version of the truncated octahedron.
We will soon create another pull request for this one. :)
For this truncated octahedron model version written with both .py + .c code, my understanding is that because no points in the gauss-legendre quadrature correspond to a singularity direction, the code is running well without error messages.

1./((qz*qz-qx*qx)*(qz*qz-qy*qy))*((qz-qx)*sin(qz*(1.-t)-qx*t)+(qz+qx)*sin(qz*(1.-t)+qx*t));

const double BB = 1./((qz*qz-qx*qx)*(qz*qz-qy*qy))*((qz-qy)*sin(qz*(1.-t)-qy*t)+(qz+qy)*sin(qz*(1.-t)+qy*t))+
1./((qx*qx-qy*qy)*(qx*qx-qz*qz))*((qx-qy)*sin(qx*(1.-t)-qy*t)+(qx+qy)*sin(qx*(1.-t)+qy*t));

const double CC = 1./((qx*qx-qy*qy)*(qx*qx-qz*qz))*((qx-qz)*sin(qx*(1.-t)-qz*t)+(qx+qz)*sin(qx*(1.-t)+qz*t))+
1./((qy*qy-qz*qz)*(qy*qy-qx*qx))*((qy-qz)*sin(qy*(1.-t)-qz*t)+(qy+qz)*sin(qy*(1.-t)+qz*t));


// normalisation to 1. of AP at q = 0. Division by a Factor 4/3.
const double AP = 6./(1.-3*(1.-t)*(1.-t)*(1.-t))*(AA+BB+CC);

inner_sum += GAUSS_W[j] * AP * AP;


}
inner_sum = 0.5 * (v2b-v2a) * inner_sum;
outer_sum += GAUSS_W[i] * inner_sum * sin_theta;
}

double answer = 0.5*(v1b-v1a)*outer_sum;

// The factor 2 appears because the theta integral has been defined between
// 0 and pi/2, instead of 0 to pi.
answer /= M_PI_2; //Form factor P(q)

// Multiply by contrast^2 and volume^2
// contrast
const double s = (sld-solvent_sld);
// volume
// s *= form_volume(length_a, b2a_ratio,c2a_ratio, t);
answer *= square(s*form_volume(length_a, b2a_ratio,c2a_ratio, t));

// Convert from [1e-12 A-1] to [cm-1]
answer *= 1.0e-4;

if (isnan(answer) || isinf(answer)) {
return 0.0;
}

return answer;
}

// Fq() is called because option "have_Fq = True" is set to True in the Python file
static void
Fq(double q,
double *F1,
double *F2,
double sld,
double solvent_sld,
double length_a,
double b2a_ratio,
double c2a_ratio,
double t)
{
const double length_b = length_a * b2a_ratio;
const double length_c = length_a * c2a_ratio;


//Integration limits to use in Gaussian quadrature
const double v1a = 0.0;
const double v1b = M_PI_2; //theta integration limits
const double v2a = 0.0;
const double v2b = M_PI_2; //phi integration limits

double outer_sum_F1 = 0.0;
double outer_sum_F2 = 0.0;

for(int i=0; i<GAUSS_N; i++) {
const double theta = 0.5 * ( GAUSS_Z[i]*(v1b-v1a) + v1a + v1b );
double sin_theta, cos_theta;
SINCOS(theta, sin_theta, cos_theta);
Copy link
Contributor

Choose a reason for hiding this comment

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

Use the following to save a little work in the inner loop:

const double qab = q * sin_theta;
const double qc = q * cos_theta;

then in the inner loop:

const double qa = qab * cos_phi;
const double qb = qab * sin_phi;

Intermediate variables related to qc can also be set in the outer loop.

Copy link
Contributor

Choose a reason for hiding this comment

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

We kept the initial code here, but added comment lines to explain the notations between rescaled components and initial ones.


double inner_sum_F1 = 0.0;
double inner_sum_F2 = 0.0;
for(int j=0; j<GAUSS_N; j++) {
double phi = 0.5 * ( GAUSS_Z[j]*(v2b-v2a) + v2a + v2b );
double sin_phi, cos_phi;
SINCOS(phi, sin_phi, cos_phi);

//HERE: Octahedron formula
// q is the modulus of the scattering vector in [A-1]
// NOTE: capital QX QY QZ are the three components in [A-1] of the scattering vector
// NOTE: qx qy qz are rescaled components (no unit) for computing AA, BB and CC terms
const double Qx = q * sin_theta * cos_phi;
const double Qy = q * sin_theta * sin_phi;
const double Qz = q * cos_theta;
const double qx = Qx * length_a;
const double qy = Qy * length_b;
const double qz = Qz * length_c;
const double AA = 1./(2*(qy*qy-qz*qz)*(qy*qy-qx*qx))*((qy-qx)*sin(qy*(1.-t)-qx*t)+(qy+qx)*sin(qy*(1.-t)+qx*t))+
1./(2*(qz*qz-qx*qx)*(qz*qz-qy*qy))*((qz-qx)*sin(qz*(1.-t)-qx*t)+(qz+qx)*sin(qz*(1.-t)+qx*t));

const double BB = 1./(2*(qz*qz-qx*qx)*(qz*qz-qy*qy))*((qz-qy)*sin(qz*(1.-t)-qy*t)+(qz+qy)*sin(qz*(1.-t)+qy*t))+
1./(2*(qx*qx-qy*qy)*(qx*qx-qz*qz))*((qx-qy)*sin(qx*(1.-t)-qy*t)+(qx+qy)*sin(qx*(1.-t)+qy*t));

const double CC = 1./(2*(qx*qx-qy*qy)*(qx*qx-qz*qz))*((qx-qz)*sin(qx*(1.-t)-qz*t)+(qx+qz)*sin(qx*(1.-t)+qz*t))+
1./(2*(qy*qy-qz*qz)*(qy*qy-qx*qx))*((qy-qz)*sin(qy*(1.-t)-qz*t)+(qy+qz)*sin(qy*(1.-t)+qz*t));

// normalisation to 1. of AP at q = 0. Division by a Factor 4/3.
const double AP = 6./(1.-3*(1.-t)*(1.-t)*(1.-t))*(AA+BB+CC);


inner_sum_F1 += GAUSS_W[j] * AP;
inner_sum_F2 += GAUSS_W[j] * AP * AP;

}
inner_sum_F1 = 0.5 * (v2b-v2a) * inner_sum_F1;
inner_sum_F2 = 0.5 * (v2b-v2a) * inner_sum_F2;
outer_sum_F1 += GAUSS_W[i] * inner_sum_F1 * sin_theta;
outer_sum_F2 += GAUSS_W[i] * inner_sum_F2 * sin_theta;
}

outer_sum_F1 *= 0.5*(v1b-v1a);
outer_sum_F2 *= 0.5*(v1b-v1a);

// The factor 2 appears because the theta integral has been defined between
// 0 and pi/2, instead of 0 to pi.
outer_sum_F1 /= M_PI_2;
outer_sum_F2 /= M_PI_2;

// Multiply by contrast and volume
// contrast
const double s = (sld-solvent_sld);
// volume
// s *= form_volume(length_a, b2a_ratio,c2a_ratio, t);

// Convert from [1e-12 A-1] to [cm-1]
*F1 = 1e-2 * s * form_volume(length_a, b2a_ratio,c2a_ratio, t) * outer_sum_F1;
*F2 = 1e-4 * square(s * form_volume(length_a, b2a_ratio,c2a_ratio, t)) * outer_sum_F2;

if (isnan(*F1) || isinf(*F1)) {
*F1 = 0.0;
}
if (isnan(*F2) || isinf(*F2)) {
*F2 = 0.0;
}
}


static double
Iqabc(double qa, double qb, double qc,
double sld,
double solvent_sld,
double length_a,
double b2a_ratio,
double c2a_ratio,
double t)
{
const double length_b = length_a * b2a_ratio;
const double length_c = length_a * c2a_ratio;


//HERE: Octahedron formula
// NOTE: qa qb qc are the three components in [A-1] of the scattering vector
// NOTE: qx qy qz are rescaled components (no unit) for computing AA, BB and CC terms
const double qx = qa * length_a;
const double qy = qb * length_b;
const double qz = qc * length_c;
Copy link
Contributor

Choose a reason for hiding this comment

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

My convention has been to use (qx, qy, qz) for the shape rotated relative to the beam and (qa, qb, qc) for the shape in canonical orientation with beam along the c axis. Furthermore, your qx variable is unitless, not 1/Å.

You may be able help the compiler using even more intermediate variables:

const double A = qa * length_a;
const double Asq = square(A);
const double At = A * t;
const double Atc = A * (1 - t);
...

This makes the structure of the equations easier to see:

const double AA =
      ((B-A)*sin(Btc-At) + (B+A)*sin(Btc+At)) / (2*(Bsq-Csq)*(Bsq-Asq))
    + ((C-A)*sin(Ctc-At) + (C+A)*sin(Ctc+At)) / (2*(Csq-Asq)*(Csq-Bsq));
...

You could further move the factor of 1/2 from each of these equations to the AP equation:

const double AP = 3 * (AA + BB + CC) / (1 - 3*cube(1-t));

Copy link
Contributor

Choose a reason for hiding this comment

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

Dear Paul,
Thank you so much for your input and clarifications about notations for orientation !
We will improve with Sara this .c file to make it more readable and update it on the branch in sasmodels (adding_trOh_model_pythonC).
Note that we will create soon a new pull request with a pure python model for the same shape (truncated octahedron). :)

const double AA = 1./(2*(qy*qy-qz*qz)*(qy*qy-qx*qx))*((qy-qx)*sin(qy*(1.-t)-qx*t)+(qy+qx)*sin(qy*(1.-t)+qx*t))+
1./(2*(qz*qz-qx*qx)*(qz*qz-qy*qy))*((qz-qx)*sin(qz*(1.-t)-qx*t)+(qz+qx)*sin(qz*(1.-t)+qx*t));

const double BB = 1./(2*(qz*qz-qx*qx)*(qz*qz-qy*qy))*((qz-qy)*sin(qz*(1.-t)-qy*t)+(qz+qy)*sin(qz*(1.-t)+qy*t))+
1./(2*(qx*qx-qy*qy)*(qx*qx-qz*qz))*((qx-qy)*sin(qx*(1.-t)-qy*t)+(qx+qy)*sin(qx*(1.-t)+qy*t));

const double CC = 1./(2*(qx*qx-qy*qy)*(qx*qx-qz*qz))*((qx-qz)*sin(qx*(1.-t)-qz*t)+(qx+qz)*sin(qx*(1.-t)+qz*t))+
1./(2*(qy*qy-qz*qz)*(qy*qy-qx*qx))*((qy-qz)*sin(qy*(1.-t)-qz*t)+(qy+qz)*sin(qy*(1.-t)+qz*t));

// normalisation to 1. of AP at q = 0. Division by a Factor 4/3.
const double AP = 6./(1.-3*(1.-t)*(1.-t)*(1.-t))*(AA+BB+CC);

// Multiply by contrast and volume
// contrast
const double s = (sld-solvent_sld);
// volume
// s *= form_volume(length_a, b2a_ratio,c2a_ratio, t);

// Convert from [1e-12 A-1] to [cm-1]
double answer = 1.0e-4 * square(s * form_volume(length_a, b2a_ratio,c2a_ratio, t) * AP);
if (isnan(answer) || isinf(answer)) {
return 0.0;
}

return answer;
}



Loading