Skip to content

Commit

Permalink
Merge pull request #98 from SCIInstitute/weights_assignment_fix
Browse files Browse the repository at this point in the history
Weight assignment fix
  • Loading branch information
jessdtate authored Feb 24, 2022
2 parents 58b6d43 + 05be2e1 commit 5a8e061
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 162 deletions.
32 changes: 28 additions & 4 deletions UncertainSCI/pce.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,29 @@ def set_samples(self, samples):
'have wrong dimension')

self.samples = samples
self.set_weights()

def set_weights(self):
"""Sets weights based on assigned samples.
"""
if self.samples is None:
raise RuntimeError("PCE weights cannot be set unless samples are set first.""")

if self.sampling.lower() == 'greedy-induced':
self.weights = self.christoffel_weights()
elif self.sampling.lower() == 'gq':
M = self.sampling_options.get('M')
if M is None:
raise ValueError("The sampling option 'M' must be specified for Gauss quadrature sampling.")

_, self.weights = self.distribution.polys.tensor_gauss_quadrature(M)

elif self.sampling.lower() == 'gq-induced':
self.weights = self.christoffel_weights()

else:
raise ValueError("Unsupported sample type '{0}' for input\
sample_type".format(self.sampling))

def map_to_standard_space(self, q):
"""Maps parameter values from model space to standard space.
Expand Down Expand Up @@ -211,8 +234,6 @@ def generate_samples(self, **kwargs):

self.samples = self.map_to_model_space(x)

self.weights = self.christoffel_weights()

elif self.sampling.lower() == 'gq':

M = self.sampling_options.get('M')
Expand All @@ -221,7 +242,9 @@ def generate_samples(self, **kwargs):

p_standard, w = self.distribution.polys.tensor_gauss_quadrature(M)
self.samples = self.map_to_model_space(p_standard)
self.weights = w
# We do the following in the call to self.set_weights() below. A
# little more expensive, but makes for more transparent control structure.
#self.weights = w

elif self.sampling.lower() == 'gq-induced':

Expand All @@ -232,12 +255,13 @@ def generate_samples(self, **kwargs):
p_standard = self.distribution.opolys.idist_gq_sampling(K, self.indices, M=self.sampling_options.get('M'))

self.samples = self.map_to_model_space(p_standard)
self.weights = self.christoffel_weights()

else:
raise ValueError("Unsupported sample type '{0}' for input\
sample_type".format(self.sampling))

self.set_weights()

def integration_weights(self):
"""
Generates sample weights associated to integration."
Expand Down
178 changes: 20 additions & 158 deletions demos/build_pce_normal.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,193 +5,55 @@

from UncertainSCI.distributions import NormalDistribution
from UncertainSCI.model_examples import sine_modulation
from UncertainSCI.indexing import TotalDegreeSet
from UncertainSCI.pce import PolynomialChaosExpansion
# # Distribution setup

from UncertainSCI.vis import piechart_sensitivity, quantile_plot, mean_stdev_plot

## Distribution setup: generating a correlated multivariate normal distribution
# Number of parameters
dimension = 2

# Specifies normal distribution
# Specifies statistics of normal distribution
mean = np.ones(dimension)
cov = np.random.randn(dimension, dimension)
cov = np.matmul(cov.T, cov)/(4*dimension) # Some random covariance matrix

# Normalize so that parameters with larger index have smaller importance
D = np.diag(1/np.sqrt(np.diag(cov)) * 1/(np.arange(1, dimension+1)**2))
cov = D @ cov @ D

dist = NormalDistribution(mean=mean, cov=cov, dim=dimension)

# # Indices setup
order = 10
index_set = TotalDegreeSet(dim=dimension, order=order)

print('This will query the model {0:d} times'.format(index_set.get_indices().shape[0] + 10))
# Why +10? That's the default for PolynomialChaosExpansion.build_pce_wafp

# # Initializes a pce object
pce = PolynomialChaosExpansion(index_set, dist)

# # Define model
## Define forward model
N = int(1e2) # Number of degrees of freedom of model
left = -1.
right = 1.
x = np.linspace(left, right, N)
model = sine_modulation(N=N)

# # Three equivalent ways to run the PCE model:
## Polynomial order
order = 5

## Initializes a pce object
pce = PolynomialChaosExpansion(distribution=dist, order=order, plabels=['p1', 'p2'])
pce.generate_samples()

# 1
# Generate samples and query model in one call:
pce = PolynomialChaosExpansion(index_set, dist)
lsq_residuals = pce.build(model, oversampling=10)
print('This queries the model {0:d} times'.format(pce.samples.shape[0]))

# Query model:
pce.build(model)

# The parameter samples and model evaluations are accessible:
parameter_samples = pce.samples
model_evaluations = pce.model_output

# And you could build a second PCE on the same parameter samples
pce2 = PolynomialChaosExpansion(index_set, dist)
# And if desired you could build a second PCE on the same parameter samples
pce2 = PolynomialChaosExpansion(distribution=dist, order=order)
pce2.set_samples(parameter_samples)
pce2.build(model)

# pce and pce2 have the same coefficients:
# np.linalg.norm( pce.coefficients - pce2.coefficients )

# # Postprocess PCE: mean, stdev, sensitivities, quantiles
mean = pce.mean()
stdev = pce.stdev()


# Power set of [0, 1, ..., dimension-1]
variable_interactions = list(chain.from_iterable(combinations(range(dimension), r) for r in range(1, dimension+1)))

# "Total sensitivity" is a non-partitive relative sensitivity measure per parameter.
total_sensitivity = pce.total_sensitivity()

# "Global sensitivity" is a partitive relative sensitivity measure per set of parameters.
global_sensitivity = pce.global_sensitivity(variable_interactions)



Q = 4 # Number of quantile bands to plot
dq = 0.5/(Q+1)
q_lower = np.arange(dq, 0.5-1e-7, dq)[::-1]
q_upper = np.arange(0.5 + dq, 1.0-1e-7, dq)
quantile_levels = np.append(np.concatenate((q_lower, q_upper)), 0.5)

quantiles = pce.quantile(quantile_levels, M=int(2e3))
median = quantiles[-1, :]

# # For comparison: Monte Carlo statistics
M = 1000 # Generate MC samples
p_phys = dist.MC_samples(M)
output = np.zeros([M, N])

for j in range(M):
output[j, :] = model(p_phys[j, :])

MC_mean = np.mean(output, axis=0)
MC_stdev = np.std(output, axis=0)
MC_quantiles = np.quantile(output, quantile_levels, axis=0)
MC_median = quantiles[-1, :]

# # Visualization
V = 50 # Number of MC samples to visualize

# mean +/- stdev plot
plt.plot(x, output[:V, :].T, 'k', alpha=0.8, linewidth=0.2)
plt.plot(x, mean, 'b', label='PCE mean')
plt.fill_between(x, mean-stdev, mean+stdev, interpolate=True, facecolor='red', alpha=0.5, label='PCE 1 stdev range')

plt.plot(x, MC_mean, 'b:', label='MC mean')
plt.plot(x, MC_mean+MC_stdev, 'r:', label='MC mean $\\pm$ stdev')
plt.plot(x, MC_mean-MC_stdev, 'r:')

plt.xlabel('x')
plt.title('Mean $\\pm$ standard deviation')

plt.legend(loc='lower right')

# quantile plot
plt.figure()

plt.subplot(121)
plt.plot(x, output[:V, :].T, 'k', alpha=0.8, linewidth=0.2)
plt.plot(x, median, 'b', label='PCE median')

band_mass = 1/(2*(Q+1))

for ind in range(Q):
alpha = (Q-ind) * 1/Q - (1/(2*Q))
if ind == 0:
plt.fill_between(x, quantiles[ind, :], quantiles[Q+ind, :], interpolate=True, facecolor='red',
alpha=alpha, label='{0:1.2f} probability mass (each band)'.format(band_mass))
else:
plt.fill_between(x, quantiles[ind, :], quantiles[Q+ind, :], interpolate=True, facecolor='red',
alpha=alpha)

plt.title('PCE Median + quantile bands')
plt.xlabel('x')
plt.legend(loc='lower right')

plt.subplot(122)
plt.plot(x, output[:V, :].T, 'k', alpha=0.8, linewidth=0.2)
plt.plot(x, MC_median, 'b', label='MC median')

for ind in range(Q):
alpha = (Q-ind) * 1/Q - (1/(2*Q))
if ind == 0:
plt.fill_between(x, MC_quantiles[ind, :], MC_quantiles[Q+ind, :], interpolate=True, facecolor='red',
alpha=alpha, label='{0:1.2f} probability mass (each band)'.format(band_mass))
else:
plt.fill_between(x, MC_quantiles[ind, :], MC_quantiles[Q+ind, :], interpolate=True, facecolor='red', alpha=alpha)

plt.title('MC Median + quantile bands')
plt.xlabel('x')
plt.legend(loc='lower right')

# Sensitivity pie chart, averaged over all model degrees of freedom
average_global_SI = np.sum(global_sensitivity, axis=1)/N

labels = ['[' + ' '.join(str(elem) for elem in [i+1 for i in item]) + ']' for item in variable_interactions]
_, ax = plt.subplots()
ax.pie(average_global_SI*100, labels=labels, autopct='%1.1f%%',
startangle=90)
ax.axis('equal') # Equal aspect ratio ensures that pie is drawn as a circle.
plt.title('Sensitivity due to variable interactions')





sensitivities = [total_sensitivity, global_sensitivity]

sensbounds = [[0, 1], [0, 1]]

senslabels = ['Total sensitivity', 'Global sensitivity']
dimlabels = ['Dimension 1', 'Dimension 2', 'Interaction']

fig, ax = plt.subplots(2, 3)
for row in range(2):
for col in range(2):
ax[row][col].plot(x, sensitivities[row][col,:])
ax[row][col].set_ylim(sensbounds[row])
if row==2:
ax[row][col].set(xlabel='$x$')
if col==0:
ax[row][col].set(ylabel=senslabels[row])
if row==0:
ax[row][col].set_title(dimlabels[col])
if row<2:
ax[row][col].xaxis.set_ticks([])
if col>0:
ax[row][col].yaxis.set_ticks([])

ax[1][2].plot(x, sensitivities[1][2,:])
ax[1][2].set_ylim(sensbounds[1])

## Visualization
mean_stdev_plot(pce, ensemble=50)
quantile_plot(pce, bands=3, xvals=x, xlabel='$x$')
piechart_sensitivity(pce)

plt.show()

0 comments on commit 5a8e061

Please sign in to comment.