scieee AI-readable full text Open interactive document viewer

Supporting Information for "Specific ion modulated adsorption of a Pluronic copolymer at the air–water interface"

Robertson, Hayden; Willott, Joshua; Nelson, Andrew; Prescott, Stuart; Wanless, Erica; WEBBER, GRANT

Abstract

This deposition contains the neutron reflectometry data and code required to reproduce the analysis detailed in Specific ion modulated adsorption of a Pluronic copolymer at the air–water interface. Jupyter notebooks are also presented as PDFs for ease of viewing. NR_data_files.zip Data directory containing all relevant reduced reflectivity profiles corresponding to equilibrium measurements from the Platypus reflectometer at ANSTO. Jupyter notebook “refnx_F127.ipynb” notebook required to reproduce the analysis pertaining F127 at the air–water interface.

Full text

refnx_solvent October 9, 2025 1 F127 at the air-water interface analysis using refnx This notebook provides a demonstration on how the neutron reflectivity data was modelled and optimised using refnx. Example shown here relates to 2 wt% F127 adsorbed at the air-water interface at 22 ˚C. The refnx package used here is readily available on the refnx GitHub repository at https://github.com/refnx/refnx. The refnx wheels are also available on PyPI prebuilt version on conda-forge. [1]: import pickle import numpy as np import matplotlib.pyplot as plt plt.rcParams["figure.figsize"]=(6,3) %matplotlib inline from scipy.stats import norm, truncnorm import sys from refnx.reflect import SLD, Slab, ReflectModel, MixedReflectModel from refnx.dataset import ReflectDataset as RD from refnx.analysis import Objective, CurveFitter, PDF, Parameter,␣ ↪process_chain, load_chain, Transform [2]: # Version numbers allow you to repeat the analysis on your computer and obtain␣ ↪identical results. import refnx,scipy refnx.__version__, np.version.version, scipy.version.version [2]: ('0.1.29', '1.26.4', '1.13.1') 1.1 Title the data we’re fitting [3]: fit_name ="F127_2wt_22C" 1.2 Creating a solvated model Let’s load in the data: 1 [4]: # load the data. data =RD('NR_data_files/D2O F127 2wt 22C - 57576.dat') Calculate dq_res and remove data.x_err to make the calculation faster with constant dq_res. [5]: dq_res =np.mean(data.x_err /data.x) * 100 data =RD(data=[data.x, data.y, data.y_err]) There are several different materials in our system. We will create SLD (component) objects for all of them. [6]: air =SLD(0,'air') polymer_ppo =SLD(0.35,'ppo') polymer_ppo.real.setp (vary=False, bounds=(0.33,0.37)) polymer_peo =SLD(0.67,'peo') polymer_peo.real.setp (vary=False, bounds=(0.65,0.69)) d2o =SLD(6.2,'d2o') d2o.real.setp (vary=True, bounds=(5.4,6.36)) Now we will define the components that make up our system. The interfacial structure of F127 is modelled using slab components. We will use: - One slab for the air superphase. - One slab for PPO. - One slab for PEO. - One slab for the D2O subphase. [7]: # Air slab air_l =air(0,0) # PPO slab ppo =polymer_ppo(10,10) ppo.thick.setp (vary=True, bounds=(0.5,50)) ppo.vfsolv.setp (vary=True, value=0.05, bounds=(0.005,0.5)) ppo.rough.setp (vary=True, bounds=(1,50)) # PEO slab peo =polymer_peo(80,10) peo.thick.setp (vary=True, bounds=(1,150)) peo.vfsolv.setp (vary=True, value=0.9, bounds=(0.1,1)) peo.rough.setp (vary=True, bounds=(1,100)) # D2O d2o_l =d2o(0,2) d2o_l.rough.setp (vary=True, bounds=(0.01,30)) Now we will put the components of our system into a structure. The structure represents the interface that we are modelling. We place this structure into our model and set extra model parameters related to our experimental conditions. 2 [8]: structure =air |ppo |peo |d2o_l # contracting the slab representation reduces computation time. structure.contract = 1.5 [9]: model =ReflectModel(structure) model.scale.setp(value=1, vary=True, bounds=(0.8,1.2)) model.bkg.setp(value=1e-5, vary=True, bounds=(0.9e-8,3e-4)) From our model and our data, we create an objective which includes the experimental data. [10]: objective =Objective(model, data, transform=Transform('logY')) Let’s have a look at our defined objective to check that we haven’t made any mistakes. [11]: objective.plot() [11]: (<Figure size 600x300 with 1 Axes>, <Axes: >) 1.3 Initial local optimisation To use differential_evolution or least_squares continue here. [12]: fitter =CurveFitter(objective) fitter.fit('least_squares'); [13]: fig, ax =plt.subplots() fitter.objective.plot(fig=fig) [13]: (<Figure size 600x300 with 1 Axes>, <Axes: >) 3 [14]: fitter.objective.chisqr() [14]: 469.2172816244555 [15]: for xin fitter.objective.varying_parameters(): print(x) <Parameter: 'scale' , value=0.982669 +/- 0.00403, bounds=[0.8, 1.2]> <Parameter: 'bkg' , value=1.06539e-05 +/- 5.87e-07, bounds=[9e-09, 0.0003]> <Parameter: 'ppo - thick' , value=16.3972 +/- 10.5 , bounds=[0.5, 50.0]> <Parameter: 'ppo - rough' , value=1.00006 +/- 867 , bounds=[1.0, 50.0]> <Parameter:'ppo - volfrac solvent', value=0.430234 +/- 17.8 , bounds=[0.005, 0.5]> <Parameter: 'peo - thick' , value=71.8302 +/- 307 , bounds=[1.0, 150.0]> <Parameter: 'peo - rough' , value=1.00002 +/- 895 , bounds=[1.0, 100.0]> <Parameter:'peo - volfrac solvent', value=0.89866 +/- 0.00459, bounds=[0.1, 1.0]> <Parameter: 'd2o - sld' , value=6.07323 +/- 0.00514, bounds=[5.4, 6.36]> <Parameter: 'd2o - rough' , value=11.1761 +/- 2.54 , bounds=[0.01, 30.0]> [ ]: 1.4 PT-MCMC sampling The PT-MCMC sampling that was conducted for this work was performed on a HPC. Here we provide an example of how the sampling was performed. We initialise with ‘prior’ so our starting positioning doesn’t matter, only the bounds are relevant. 4 [16]: fitter =CurveFitter(objective, ntemps=15, nwalkers=500) fitter.initialise('prior') [ ]: num_burn = 300 for iin range(num_burn): print("BURNING: %d/%d"%(i+1,num_burn)) fitter.sample(1, nthin=200); pickle.dump(fitter, open(fit_name +'_fitter.pkl','wb')) BURNING: 1/300 BURNING: 2/300 BURNING: 3/300 BURNING: 4/300 BURNING: 5/300 BURNING: 6/300 BURNING: 7/300 BURNING: 8/300 BURNING: 9/300 BURNING: 10/300 BURNING: 11/300 BURNING: 12/300 BURNING: 13/300 BURNING: 14/300 BURNING: 15/300 BURNING: 16/300 BURNING: 17/300 BURNING: 18/300 BURNING: 19/300 BURNING: 20/300 BURNING: 21/300 BURNING: 22/300 BURNING: 23/300 BURNING: 24/300 BURNING: 25/300 BURNING: 26/300 BURNING: 27/300 BURNING: 28/300 BURNING: 29/300 BURNING: 30/300 BURNING: 31/300 BURNING: 32/300 BURNING: 33/300 BURNING: 34/300 BURNING: 35/300 BURNING: 36/300 BURNING: 37/300 BURNING: 38/300 5 BURNING: 39/300 BURNING: 40/300 BURNING: 41/300 BURNING: 42/300 BURNING: 43/300 BURNING: 44/300 BURNING: 45/300 BURNING: 46/300 BURNING: 47/300 BURNING: 48/300 BURNING: 49/300 BURNING: 50/300 BURNING: 51/300 BURNING: 52/300 BURNING: 53/300 BURNING: 54/300 BURNING: 55/300 BURNING: 56/300 BURNING: 57/300 BURNING: 58/300 BURNING: 59/300 BURNING: 60/300 BURNING: 61/300 BURNING: 62/300 BURNING: 63/300 BURNING: 64/300 BURNING: 65/300 BURNING: 66/300 BURNING: 67/300 BURNING: 68/300 BURNING: 69/300 BURNING: 70/300 BURNING: 71/300 BURNING: 72/300 BURNING: 73/300 BURNING: 74/300 BURNING: 75/300 BURNING: 76/300 BURNING: 77/300 BURNING: 78/300 BURNING: 79/300 BURNING: 80/300 BURNING: 81/300 BURNING: 82/300 BURNING: 83/300 BURNING: 84/300 BURNING: 85/300 BURNING: 86/300 6 BURNING: 87/300 BURNING: 88/300 BURNING: 89/300 BURNING: 90/300 BURNING: 91/300 BURNING: 92/300 BURNING: 93/300 BURNING: 94/300 BURNING: 95/300 BURNING: 96/300 BURNING: 97/300 BURNING: 98/300 BURNING: 99/300 BURNING: 100/300 BURNING: 101/300 BURNING: 102/300 BURNING: 103/300 BURNING: 104/300 BURNING: 105/300 BURNING: 106/300 BURNING: 107/300 BURNING: 108/300 BURNING: 109/300 BURNING: 110/300 BURNING: 111/300 BURNING: 112/300 BURNING: 113/300 BURNING: 114/300 BURNING: 115/300 BURNING: 116/300 BURNING: 117/300 BURNING: 118/300 BURNING: 119/300 BURNING: 120/300 BURNING: 121/300 BURNING: 122/300 BURNING: 123/300 BURNING: 124/300 BURNING: 125/300 BURNING: 126/300 BURNING: 127/300 BURNING: 128/300 BURNING: 129/300 BURNING: 130/300 BURNING: 131/300 BURNING: 132/300 BURNING: 133/300 BURNING: 134/300 7 BURNING: 135/300 BURNING: 136/300 BURNING: 137/300 BURNING: 138/300 BURNING: 139/300 BURNING: 140/300 BURNING: 141/300 BURNING: 142/300 BURNING: 143/300 BURNING: 144/300 BURNING: 145/300 BURNING: 146/300 BURNING: 147/300 BURNING: 148/300 BURNING: 149/300 BURNING: 150/300 BURNING: 151/300 BURNING: 152/300 BURNING: 153/300 BURNING: 154/300 BURNING: 155/300 BURNING: 156/300 BURNING: 157/300 BURNING: 158/300 BURNING: 159/300 BURNING: 160/300 BURNING: 161/300 BURNING: 162/300 BURNING: 163/300 BURNING: 164/300 BURNING: 165/300 BURNING: 166/300 BURNING: 167/300 BURNING: 168/300 BURNING: 169/300 BURNING: 170/300 BURNING: 171/300 BURNING: 172/300 BURNING: 173/300 BURNING: 174/300 BURNING: 175/300 BURNING: 176/300 BURNING: 177/300 BURNING: 178/300 BURNING: 179/300 BURNING: 180/300 BURNING: 181/300 BURNING: 182/300 8 BURNING: 183/300 BURNING: 184/300 BURNING: 185/300 BURNING: 186/300 BURNING: 187/300 BURNING: 188/300 BURNING: 189/300 BURNING: 190/300 BURNING: 191/300 BURNING: 192/300 BURNING: 193/300 BURNING: 194/300 BURNING: 195/300 BURNING: 196/300 BURNING: 197/300 BURNING: 198/300 BURNING: 199/300 BURNING: 200/300 BURNING: 201/300 BURNING: 202/300 BURNING: 203/300 BURNING: 204/300 BURNING: 205/300 BURNING: 206/300 BURNING: 207/300 BURNING: 208/300 BURNING: 209/300 BURNING: 210/300 BURNING: 211/300 BURNING: 212/300 BURNING: 213/300 BURNING: 214/300 BURNING: 215/300 BURNING: 216/300 BURNING: 217/300 BURNING: 218/300 BURNING: 219/300 BURNING: 220/300 BURNING: 221/300 BURNING: 222/300 BURNING: 223/300 BURNING: 224/300 BURNING: 225/300 BURNING: 226/300 BURNING: 227/300 BURNING: 228/300 BURNING: 229/300 BURNING: 230/300 9 ax[0].errorbar(data.x, data.y, data.y_err, ms=5, lw=0, elinewidth=2, c='grey') saved_params =np.array(objective.parameters) pvecs =fitter.chain[-1,0] for pvec in pvecs: objective.setp(pvec) ax[0].plot(data.x, objective.generative(), color='r', alpha=0.01, zorder=2) z, sld =objective.model.structure.sld_profile() ax[1].plot(z, sld, color='r', alpha=0.01) objective.setp(saved_params) ax[0].set_yscale('log') ax[0].set_ylabel('Reflectivity') ax[1].set_ylabel('SLD, $\\times 10^{-6}$ Å') ax[1].set_xlabel('Distance, Å') ax[0].set_xlabel('$Q$, Å$^{-1}$') [ ]: Text(0.5, 0, '$Q$, Å$^{-1}$') [19]: for xin fitter.objective.varying_parameters(): print(x) <Parameter: 'scale' , value=0.982598 +/- 0.00476, bounds=[0.8, 1.2]> <Parameter: 'bkg' , value=8.67999e-06 +/- 2.08e-07, bounds=[9e-09, 0.0003]> <Parameter: 'ppo - thick' , value=14.299 +/- 0.475, bounds=[0.5, 50.0]> <Parameter: 'ppo - rough' , value=2.70204 +/- 0.361, bounds=[1.0, 50.0]> <Parameter:'ppo - volfrac solvent', value=0.372202 +/- 0.0379, bounds=[0.005, 16 0.5]> <Parameter: 'peo - thick' , value=72.9662 +/- 1.11 , bounds=[1.0, 150.0]> <Parameter: 'peo - rough' , value=4.39383 +/- 0.656, bounds=[1.0, 100.0]> <Parameter:'peo - volfrac solvent', value=0.895801 +/- 0.00234, bounds=[0.1, 1.0]> <Parameter: 'd2o - sld' , value=6.07297 +/- 0.0062, bounds=[5.5, 6.4]> <Parameter:'water_poly_rough', value=12.7351 +/- 0.786, bounds=[1.0, 100.0]> [ ]: 17