TL:DR
How can one implement a bifurcation diagram of a seasonally forced epidemiological model such as SEIR (susceptible, exposed, infected, recovered) in Python? I already know how to implement the model itself and display a sampled time series (see this stackoverflow question), but I am struggling with reproducing a bifurcation figure from a textbook.
Context and My Attempt
I am trying to reproduce figures from the book "Modeling Infectious Diseases in Humans and Animals" (Keeling 2007) to both validate my implementations of models and to learn/visualize how different model parameters affect the evolution of a dynamical system. Below is the textbook figure.
I have found implementations of bifurcation diagrams for examples using the logistic map (see this ipython cookbook this pythonalgos bifurcation, and this stackoverflow question). My main takeaway from these implementations was that a single point on the bifurcation diagram has an x-component equal to some particular value of the varied parameter (e.g., Beta 1 = 0.025) and its y-component is the solution (numerical or otherwise) at time t for a given model/function. I use this logic to implement the plot_bifurcation function in the code section at the end of this question.
Questions
Why do my panel outputs not match those in the figure? I assume I can't try to reproduce the bifurcation diagram from the textbook without my panels matching the output in the textbook.
I have tried to implement a function to produce a bifurcation diagram, but the output looks really strange. Am I misunderstanding something about the bifurcation diagram?
NOTE: I receive no warnings/errors during code execution.
Code to Reproduce my Figures
from typing import Callable, Dict, List, Optional, Any
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
def seasonal_seir(y: List, t: List, params: Dict[str, Any]):
"""Seasonally forced SEIR model.
Function parameters much match with those required
by `scipy.integrate.odeint`
Args:
y: Initial conditions.
t: Timesteps over which numerical solution will be computed.
params: Dict with the following key-value pairs:
beta_zero -- Average transmission rate.
beta_one -- Amplitude of seasonal forcing.
omega -- Period of forcing.
mu -- Natural mortality rate.
sigma -- Latent period for infection.
gamma -- Recovery from infection term.
Returns:
Tuple whose components are the derivatives of the
susceptible, exposed, and infected state variables
w.r.t to time.
References:
[SEIR Python Program from Textbook](http://homepages.warwick.ac.uk/~masfz/ModelingInfectiousDiseases/Chapter2/Program_2.6/Program_2_6.py)
[Seasonally Forced SIR Program from Textbook](http://homepages.warwick.ac.uk/~masfz/ModelingInfectiousDiseases/Chapter5/Program_5.1/Program_5_1.py)
"""
beta_zero = params['beta_zero']
beta_one = params['beta_one']
omega = params['omega']
mu = params['mu']
sigma = params['sigma']
gamma = params['gamma']
s, e, i = y
beta = beta_zero*(1 + beta_one*np.cos(omega*t))
sdot = mu - (beta * i + mu)*s
edot = beta*s*i - (mu + sigma)*e
idot = sigma*e - (mu + gamma)*i
return sdot, edot, idot
def plot_panels(
model: Callable,
model_params: Dict,
panel_param_space: List,
panel_param_name: str,
initial_conditions: List,
timesteps: List,
odeint_kwargs: Optional[Dict] = dict(),
x_ticks: Optional[List] = None,
time_slice: Optional[slice] = None,
state_var_ix: Optional[int] = None,
log_scale: bool = False):
"""Plot panels that are samples of the parameter space for bifurcation.
Args:
model: Function that models dynamical system. Returns dydt.
model_params: Dict whose key-value pairs are the names
of parameters in a given model and the values of those parameters.
bifurcation_parameter_space: List of varied bifurcation parameters.
bifuraction_parameter_name: The name o the bifurcation parameter.
initial_conditions: Initial conditions for numerical integration.
timesteps: Timesteps for numerical integration.
odeint_kwargs: Key word args for numerical integration.
state_var_ix: State variable in solutions to use for plot.
time_slice: Restrict the bifurcation plot to a subset
of the all solutions for numerical integration timestep space.
Returns:
Figure and axes tuple.
"""
# Set default ticks
if x_ticks is None:
x_ticks = timesteps
# Create figure
fig, axs = plt.subplots(ncols=len(panel_param_space))
# For each parameter that is varied for a given panel
# compute numerical solutions and plot
for ix, panel_param in enumerate(panel_param_space):
# update model parameters with the varied parameter
model_params[panel_param_name] = panel_param
# Compute solutions
solutions = odeint(
model,
initial_conditions,
timesteps,
args=(model_params,),
**odeint_kwargs)
# If there is a particular solution of interst, index it
# otherwise squeeze last dimension so that [T, 1] --> [T]
# where T is the max number of timesteps
if state_var_ix is not None:
solutions = solutions[:, state_var_ix]
elif state_var_ix is None and solutions.shape[-1] == 1:
solutions = np.squeeze(solutions)
else:
raise ValueError(
f'solutions to model are rank-2 tensor of shape {solutions.shape}'
' with the second dimension greater than 1. You must pass'
' a value to :param state_var_ix:')
# Slice the solutions based on the desired time range
if time_slice is not None:
solutions = solutions[time_slice]
# Natural log scale the results
if log_scale:
solutions = np.log(solutions)
# Plot the results
axs[ix].plot(x_ticks, solutions)
return fig, axs
def plot_bifurcation(
model: Callable,
model_params: Dict,
bifurcation_parameter_space: List,
bifurcation_param_name: str,
initial_conditions: List,
timesteps: List,
odeint_kwargs: Optional[Dict] = dict(),
state_var_ix: Optional[int] = None,
time_slice: Optional[slice] = None,
log_scale: bool = False):
"""Plot a bifurcation diagram of state variable from dynamical system.
Args:
model: Function that models system. Returns dydt.
model_params: Dict whose key-value pairs are the names
of parameters in a given model and the values of those parameters.
bifurcation_parameter_space: List of varied bifurcation parameters.
bifuraction_parameter_name: The name o the bifurcation parameter.
initial_conditions: Initial conditions for numerical integration.
timesteps: Timesteps for numerical integration.
odeint_kwargs: Key word args for numerical integration.
state_var_ix: State variable in solutions to use for plot.
time_slice: Restrict the bifurcation plot to a subset
of the all solutions for numerical integration timestep space.
log_scale: Flag to natural log scale solutions.
Returns:
Figure and axes tuple.
"""
# Track the solutions for each parameter
parameter_x_time_matrix = []
# Iterate through parameters
for param in bifurcation_parameter_space:
# Update the parameter dictionary for the model
model_params[bifurcation_param_name] = param
# Compute the solutions to the model using
# dictionary of parameters (including the bifurcation parameter)
solutions = odeint(
model,
initial_conditions,
timesteps,
args=(model_params, ),
**odeint_kwargs)
# If there is a particular solution of interst, index it
# otherwise squeeze last dimension so that [T, 1] --> [T]
# where T is the max number of timesteps
if state_var_ix is not None:
solutions = solutions[:, state_var_ix]
elif state_var_ix is None and solutions.shape[-1] == 1:
solutions = np.squeeze(solutions)
else:
raise ValueError(
f'solutions to model are rank-2 tensor of shape {solutions.shape}'
' with the second dimension greater than 1. You must pass'
' a value to :param state_var_ix:')
# Update the parent list of solutions for this particular
# bifurcation parameter
parameter_x_time_matrix.append(solutions)
# Cast to numpy array
parameter_x_time_matrix = np.array(parameter_x_time_matrix)
# Transpose: Bifurcation plots Function Output vs. Parameter
# This line ensures that each row in the matrix is the solution
# to a particular state variable in the system of ODEs
# a timestep t
# and each column is that solution for a particular value of
# the (varied) bifurcation parameter of interest
time_x_parameter_matrix = np.transpose(parameter_x_time_matrix)
# Slice the iterations to display to a smaller range
if time_slice is not None:
time_x_parameter_matrix = time_x_parameter_matrix[time_slice]
# Make bifurcation plot
fig, ax = plt.subplots()
# For the solutions vector at timestep plot the bifurcation
# NOTE: The elements of the solutions vector represent the
# numerical solutions at timestep t for all varied parameters
# in the parameter space
# e.g.,
# t beta1=0.025 beta1=0.030 .... beta1=0.30
# 0 solution00 solution01 .... solution0P
for sol_at_time_t_for_all_params in time_x_parameter_matrix:
if log_scale:
sol_at_time_t_for_all_params = np.log(sol_at_time_t_for_all_params)
ax.plot(
bifurcation_parameter_space,
sol_at_time_t_for_all_params,
',k',
alpha=0.25)
return fig, ax
# Define initial conditions based on figure
s0 = 6e-2
e0 = i0 = 1e-3
initial_conditions = [s0, e0, i0]
# Define model parameters based on figure
# NOTE: omega is not mentioned in the figure, but
# omega is defined elsewhere as 2pi/365
days_per_year = 365
mu = 0.02/days_per_year
beta_zero = 1250
sigma = 1/8
gamma = 1/5
omega = 2*np.pi / days_per_year
model_params = dict(
beta_zero=beta_zero,
omega=omega,
mu=mu,
sigma=sigma,
gamma=gamma)
# Define timesteps
nyears = 200
ndays = nyears * days_per_year
timesteps = np.arange(1, ndays + 1, 1)
# Define different levels of seasonality (from figure)
beta_ones = [0.025, 0.05, 0.25]
# Define the time range to actually show on the plot
min_year = 190
max_year = 200
# Create a slice of the iterations to display on the diagram
time_slice = slice(min_year*days_per_year, max_year*days_per_year)
# Get the xticks to display on the plot based on the time slice
x_ticks = timesteps[time_slice]/days_per_year
# Plot the panels using the infected state variable ix
infection_ix = 2
# Plot the panels
panel_fig, panel_ax = plot_panels(
model=seasonal_seir,
model_params=model_params,
panel_param_space=beta_ones,
panel_param_name='beta_one',
initial_conditions=initial_conditions,
timesteps=timesteps,
odeint_kwargs=dict(hmax=5),
x_ticks=x_ticks,
time_slice=time_slice,
state_var_ix=infection_ix,
log_scale=False)
# Label the panels
panel_fig.suptitle('Attempt to Reproduce Panels from Keeling 2007')
panel_fig.supxlabel('Time (years)')
panel_fig.supylabel('Fraction Infected')
panel_fig.set_size_inches(15, 8)
# Plot bifurcation
bi_fig, bi_ax = plot_bifurcation(
model=seasonal_seir,
model_params=model_params,
bifurcation_parameter_space=np.linspace(0.025, 0.3),
bifurcation_param_name='beta_one',
initial_conditions=initial_conditions,
timesteps=timesteps,
odeint_kwargs={'hmax':5},
state_var_ix=infection_ix,
time_slice=time_slice,
log_scale=False)
# Label the bifurcation
bi_fig.suptitle('Attempt to Reproduce Bifurcation Diagram from Keeling 2007')
bi_fig.supxlabel(r'$\beta_1$')
bi_fig.supylabel('Fraction Infected')
bi_fig.set_size_inches(15, 8)
The answer to this questions is here on the Computational Science stack exchange. All credit to Lutz Lehmann.
Related
How to generate "lower" and "upper" predictions, not just "yhat"?
import statsmodels
from statsmodels.tsa.arima.model import ARIMA
assert statsmodels.__version__ == '0.12.0'
arima = ARIMA(df['value'], order=order)
model = arima.fit()
Now I can generate "yhat" predictions
yhat = model.forecast(123)
and get confidence intervals for model parameters (but not for predictions):
model.conf_int()
but how to generate yhat_lower and yhat_upper predictions?
In general, the forecast and predict methods only produce point predictions, while the get_forecast and get_prediction methods produce full results including prediction intervals.
In your example, you can do:
forecast = model.get_forecast(123)
yhat = forecast.predicted_mean
yhat_conf_int = forecast.conf_int(alpha=0.05)
If your data is a Pandas Series, then yhat_conf_int will be a DataFrame with two columns, lower <name> and upper <name>, where <name> is the name of the Pandas Series.
If your data is a numpy array (or Python list), then yhat_conf_int will be an (n_forecasts, 2) array, where the first column is the lower part of the interval and the second column is the upper part.
To generate prediction intervals as opposed to confidence intervals (which you have neatly made the distinction between, and is also presented in Hyndman's blog post on the difference between prediction intervals and confidence intervals), then you can follow the guidance available in this answer.
You could also try to compute bootstrapped prediction intervals, which is laid out in this answer.
Below, is my attempt at implementing this (I'll update it when I get the chance to check it in more detail):
def bootstrap_prediction_interval(y_train: Union[list, pd.Series],
y_fit: Union[list, pd.Series],
y_pred_value: float,
alpha: float = 0.05,
nbootstrap: int = None,
seed: int = None):
"""
Bootstraps a prediction interval around an ARIMA model's predictions.
Method presented clearly here:
- https://stats.stackexchange.com/a/254321
Also found through here, though less clearly:
- https://otexts.com/fpp3/prediction-intervals.html
Can consider this to be a time-series version of the following generalisation:
- https://saattrupdan.github.io/2020-03-01-bootstrap-prediction/
:param y_train: List or Series of training univariate time-series data.
:param y_fit: List or Series of model fitted univariate time-series data.
:param y_pred_value: Float of the model predicted univariate time-series you want to compute P.I. for.
:param alpha: float = 0.05, the prediction uncertainty.
:param nbootstrap: integer = 1000, the number of bootstrap sampling of the residual forecast error.
Rules of thumb provided here:
- https://stats.stackexchange.com/questions/86040/rule-of-thumb-for-number-of-bootstrap-samples
:param seed: Integer to specify if you want deterministic sampling.
:return: A list [`lower`, `pred`, `upper`] with `pred` being the prediction
of the model and `lower` and `upper` constituting the lower- and upper
bounds for the prediction interval around `pred`, respectively.
"""
# get number of samples
n = len(y_train)
# compute the forecast errors/resid
fe = y_train - y_fit
# get percentile bounds
percentile_lower = (alpha * 100) / 2
percentile_higher = 100 - percentile_lower
if nbootstrap is None:
nbootstrap = np.sqrt(n).astype(int)
if seed is None:
rng = np.random.default_rng()
else:
rng = np.random.default_rng(seed)
# bootstrap sample from errors
error_bootstrap = []
for _ in range(nbootstrap):
idx = rng.integers(low=n)
error_bootstrap.append(fe[idx])
# get lower and higher percentiles of sampled forecast errors
fe_lower = np.percentile(a=error_bootstrap, q=percentile_lower)
fe_higher = np.percentile(a=error_bootstrap, q=percentile_higher)
# compute P.I.
pi = [y_pred_value + fe_lower, y_pred_value, y_pred_value + fe_higher]
return pi
using ARIMA you need to include seasonality and exogenous variables in the model yourself. While using SARIMA (Seasonal ARIMA) or SARIMAX (also for exogenous factors) implementation give C.I. to summary_frame:
import statsmodels.api as sm
import matplotlib.pyplot as plt
import pandas as pd
dta = sm.datasets.sunspots.load_pandas().data[['SUNACTIVITY']]
dta.index = pd.Index(pd.date_range("1700", end="2009", freq="A"))
print(dta)
print("init data:\n")
dta.plot(figsize=(12,4));
plt.show()
##print("SARIMAX(dta, order=(2,0,0), trend='c'):\n")
result = sm.tsa.SARIMAX(dta, order=(2,0,0), trend='c').fit(disp=False)
print(">>> result.params:\n", result.params, "\n")
##print("SARIMA_model.plot_diagnostics:\n")
result.plot_diagnostics(figsize=(15,12))
plt.show()
# summary stats of residuals
print(">>> residuals.describe:\n", result.resid.describe(), "\n")
# Out-of-sample forecasts are produced using the forecast or get_forecast methods from the results object
# The get_forecast method is more general, and also allows constructing confidence intervals.
fcast_res1 = result.get_forecast()
# specify that we want a confidence level of 90%
print(">>> forecast summary at alpha=0.01:\n", fcast_res1.summary_frame(alpha=0.10), "\n")
# plot forecast
fig, ax = plt.subplots(figsize=(15, 5))
# Construct the forecasts
fcast = result.get_forecast('2010Q4').summary_frame()
print(fcast)
fcast['mean'].plot(ax=ax, style='k--')
ax.fill_between(fcast.index, fcast['mean_ci_lower'], fcast['mean_ci_upper'], color='k', alpha=0.1);
fig.tight_layout()
plt.show()
docs: "The forecast above may not look very impressive, as it is almost a straight line. This is because this is a very simple, univariate forecasting model. Nonetheless, keep in mind that these simple forecasting models can be extremely competitive"
p.s. here " you can use it in a non-seasonal way by setting the seasonal terms to zero."
I am trying to fit the parameters of a transit light curve.
I have observed transit light curve data and I am using a .py in python that through 4 parameters (period, a(semi-major axis), inclination, planet radius) returns a model transit light curve. I would like to minimize the residual between these two light curves. This is what I am trying to do: First - Estimate a max likelihood using method = "L-BFGS-B" and then apply the mcmc using emcee to estimate the uncertainties.
The code:
p = lmfit.Parameters()
p.add_many(('per', 2.), ('inc', 90.), ('a', 5.), ('rp', 0.1))
per_b = [1., 3.]
a_b = [4., 6.]
inc_b = [88., 90.]
rp_b = [0.1, 0.3]
bounds = [(per_b[0], per_b[1]), (inc_b[0], inc_b[1]), (a_b[0], a_b[1]), (rp_b[0], rp_b[1])]
def residual(p):
v = p.valuesdict()
eclipse.criarEclipse(v['per'], v['a'], v['inc'], v['rp'])
lc0 = numpy.array(eclipse.getCurvaLuz()) (observed flux data)
ts0 = numpy.array(eclipse.getTempoHoras()) (observed time data)
c = numpy.linspace(min(time_phased[bb]),max(time_phased[bb]),len(time_phased[bb]),endpoint=True)
nn = interpolate.interp1d(ts0,lc0)
return nn(c) - smoothed_LC[bb] (residual between the model and the data)
Inside def residual(p) I make sure that both the observed data (time_phased[bb] and smoothed_LC[bb]) have the same size of the model transit light curve. I want it to give me the best fit values for the parameters (v['per'], v['a'], v['inc'], v['rp']).
I need your help and I appreciate your time and your attention. Kindest regards, Yuri.
Your example is incomplete, with many partial concepts and some invalid Python. This makes it slightly hard to understand your intention. If the answer below is not sufficient, update your question with a complete example.
It seems pretty clear that you want to model your data smoothed_LC[bb] (not sure what bb is) with a model for some effect of an eclipse. With that assumption, I would recommend using the lmfit.Model approach. Start by writing a function that models the data, just so you check and plot your model. I'm not entirely sure I understand everything you're doing, but this model function might look like this:
import numpy
from scipy import interpolate
from lmfit import Model
# import eclipse from somewhere....
def eclipse_lc(c, per, a, inc, p):
eclipse.criarEclipse(per, a, inc, rp)
lc0 = numpy.array(eclipse.getCurvaLuz()) # observed flux data
ts0 = numpy.array(eclipse.getTempoHoras()) # observed time data
return interpolate.interp1d(ts0,lc0)(c)
With this model function, you can build a Model:
lc_model = Model(eclipse_lc)
and then build parameters for your model. This will automatically name them after the argument names of your model function. Here, you can also give them initial values:
params = lc_model.make_params(per=2, inc=90, a=5, rp=0.1)
You wanted to place upper and lower bounds on these parameters. This is done by setting min and max parameters, not making an ordered array of bounds:
params['per'].min = 1.0
params['per'].max = 3.0
and so on. But also: setting such tight bounds is usually a bad idea. Set bounds to avoid unphysical parameter values or when it becomes evident that you need to place them.
Now, you can fit your data with this model. Well, first you need to get the data you want to model. This seems less clear from your example, but perhaps:
c_data = numpy.linspace(min(time_phased[bb]), max(time_phased[bb]),
len(time_phased[bb]), endpoint=True)
lc_data = smoothed_LC[bb]
Well: why do you need to make this c_data? Why not just use time_phased as the independent variable? Anyway, now you can fit your data to your model with your parameters:
result = lc_model(lc_data, params, c=c_data)
At this point, you can print out a report of the results and/or view or get the best-fit arrays:
print(result.fit_report())
for p in result.params.items(): print(p)
import matplotlib.pyplot as plt
plt.plot(c_data, lc_data, label='data')
plt.plot(c_data. result.best_fit, label='fit')
plt.legend()
plt.show()
Hope that helps...
I am stuck with probably a simple problem but after reading pyvista docs I am still looking for an answer. I am trying to plot a grid in which each cell will be a mesh defined as a parametric shape i.e. supertorus. In an early version of pyvista, I defined "my own" supertorus as below:
def supertorus(yScale, xScale, Height, InternalRadius, Vertical, Horizontal,
deltaX=0, deltaY=0, deltaZ=0):
# initial range for values used in parametric equation
n = 100
u = np.linspace(-np.pi, np.pi, n)
t = np.linspace(-np.pi, np.pi, n)
u, t = np.meshgrid(u, t)
# a1: Y Scale <0, 2>
a1 = yScale
# a2: X Scale <0, 2>
a2 = xScale
# a3: Height <0, 5>
a3 = Height
# a4: Internal radius <0, 5>
a4 = InternalRadius
# e1: Vertical squareness <0.25, 1>
e1 = Vertical
# e2: Horizontal squareness <0.25, 1>
e2 = Horizontal
# Definition of parametric equation for supertorus
x = a1 * (a4 + np.sign(np.cos(u)) * np.abs(np.cos(u)) ** e1) *\
np.sign(np.cos(t)) * np.abs(np.cos(t)) ** e2
y = a2 * (a4 + np.sign(np.cos(u)) * np.abs(np.cos(u)) ** e1) *\
np.sign(np.sin(t)) * np.abs(np.sin(t)) ** e2
z = a3 * np.sign(np.sin(u)) * np.abs(np.sin(u)) ** e1
grid = pyvista.StructuredGrid(x + deltaX + 5, y + deltaY + 5, z + deltaZ)
return grid
I could manipulate with deltaX, deltaY and deltaZ to position supertori at the location of my choice.
Unfortunately, this approach was not efficient and I am planning to use PyVista provided supertoroidal meshes (https://docs.pyvista.org/examples/00-load/create-parametric-geometric-objects.html?highlight=supertoroid). My question is: how I can place multiple meshes (like supertori) at the location defined by coordinates x, y, z?
I believe what you're looking for are glyphs. You can pass your own dataset as a glyph geometry that will in turn plot the dataset in each point of the supermesh. Without going into details of orienting your glyphs, colouring them according to scalars and whatnot, here's a simple "alien invasion" scenario as an example:
import numpy as np
import pyvista as pv
# get dataset for the glyphs: supertoroid in xy plane
saucer = pv.ParametricSuperToroid(ringradius=0.5, n2=1.5, zradius=0.5)
saucer.rotate_y(90)
# saucer.plot() # <-- check how a single saucer looks like
# get dataset where to put glyphs
x,y,z = np.mgrid[-1:2, -1:2, :2]
mesh = pv.StructuredGrid(x, y, z)
# construct the glyphs on top of the mesh
glyphs = mesh.glyph(geom=saucer, factor=0.3)
# glyphs.plot() # <-- simplest way to plot it
# create Plotter and add our glyphs with some nontrivial lighting
plotter = pv.Plotter(window_size=(1000, 800))
plotter.add_mesh(glyphs, color=[0.2, 0.2, 0.2], specular=1, specular_power=15)
plotter.show()
I've added some strong specular lighting to make the saucers look more menacing:
But the key point for your problem was creating the glyphs from your supermesh by passing it as the geom keyword of mesh.glyph. The other keywords such as orient and scale are useful for arrow-like glyphs where you can use the glyph to denote vectorial information of your dataset.
You've asked in comments whether it's possible to vary the glyphs along the dataset. I was certain that this was not possible, however the VTK docs clearly mention the possibility to define a collection of glyphs to use:
More than one glyph may be used by creating a table of source objects, each defining a different glyph. If a table of glyphs is defined, then the table can be indexed into by using either scalar value or vector magnitude.
It turns out that PyVista doesn't expose this functionality (yet), but the base vtk package lets us get our hands dirty. Here's a proof of concept based on DataSetFilters.glyph, which I'll float by the PyVista devs to see if there's interest in exposing this functionality.
import numpy as np
import pyvista as pv
from pyvista.core.filters import _get_output # just for this standalone example
import vtk
pyvista = pv # just for this standalone example
# below: adapted from core/filters.py
def multiglyph(dataset, orient=True, scale=True, factor=1.0,
tolerance=0.0, absolute=False, clamping=False, rng=None,
geom_datasets=None, geom_values=None):
"""Copy a geometric representation (called a glyph) to every point in the input dataset.
The glyphs may be oriented along the input vectors, and they may be scaled according to scalar
data or vector magnitude.
Parameters
----------
orient : bool
Use the active vectors array to orient the glyphs
scale : bool
Use the active scalars to scale the glyphs
factor : float
Scale factor applied to sclaing array
tolerance : float, optional
Specify tolerance in terms of fraction of bounding box length.
Float value is between 0 and 1. Default is 0.0. If ``absolute``
is ``True`` then the tolerance can be an absolute distance.
absolute : bool, optional
Control if ``tolerance`` is an absolute distance or a fraction.
clamping: bool
Turn on/off clamping of "scalar" values to range.
rng: tuple(float), optional
Set the range of values to be considered by the filter when scalars
values are provided.
geom_datasets : tuple(vtk.vtkDataSet), optional
The geometries to use for the glyphs in table mode
geom_values : tuple(float), optional
The value to assign to each geometry dataset, optional
"""
# Clean the points before glyphing
small = pyvista.PolyData(dataset.points)
small.point_arrays.update(dataset.point_arrays)
dataset = small.clean(point_merging=True, merge_tol=tolerance,
lines_to_points=False, polys_to_lines=False,
strips_to_polys=False, inplace=False,
absolute=absolute)
# Make glyphing geometry
if not geom_datasets:
arrow = vtk.vtkArrowSource()
arrow.Update()
geom_datasets = arrow.GetOutput(),
geom_values = 0,
# check if the geometry datasets are consistent
if not len(geom_datasets) == len(geom_values):
raise ValueError('geom_datasets and geom_values must have the same length!')
# TODO: other kinds of sanitization, e.g. check for sequences etc.
# Run the algorithm
alg = vtk.vtkGlyph3D()
if len(geom_values) == 1:
# use a single glyph
alg.SetSourceData(geom_datasets[0])
else:
alg.SetIndexModeToScalar()
# TODO: index by vectors?
# TODO: SetInputArrayToProcess for arbitrary arrays, maybe?
alg.SetRange(min(geom_values), max(geom_values))
# TODO: different Range?
for val, geom in zip(geom_values, geom_datasets):
alg.SetSourceData(val, geom)
if isinstance(scale, str):
dataset.active_scalars_name = scale
scale = True
if scale:
if dataset.active_scalars is not None:
if dataset.active_scalars.ndim > 1:
alg.SetScaleModeToScaleByVector()
else:
alg.SetScaleModeToScaleByScalar()
else:
alg.SetScaleModeToDataScalingOff()
if isinstance(orient, str):
dataset.active_vectors_name = orient
orient = True
if rng is not None:
alg.SetRange(rng)
alg.SetOrient(orient)
alg.SetInputData(dataset)
alg.SetVectorModeToUseVector()
alg.SetScaleFactor(factor)
alg.SetClamping(clamping)
alg.Update()
return _get_output(alg)
def example():
"""Small glyph example"""
rng = np.random.default_rng()
# get dataset for the glyphs: supertoroid in xy plane
# use N random kinds of toroids over a mesh with 27 points
N = 5
values = np.arange(N) # values for scalars to look up glyphs by
geoms = [pv.ParametricSuperToroid(n1=n1, n2=n2) for n1,n2 in rng.uniform(0.5, 2, size=(N, 2))]
for geom in geoms:
# make the disks horizontal for aesthetics
geom.rotate_y(90)
# get dataset where to put glyphs
x,y,z = np.mgrid[-1:2, -1:2, -1:2]
mesh = pv.StructuredGrid(x, y, z)
# add random scalars
mesh.point_arrays['scalars'] = rng.integers(0, N, size=x.size)
# construct the glyphs on top of the mesh; don't scale by scalars now
glyphs = multiglyph(mesh, geom_datasets=geoms, geom_values=values, scale=False, factor=0.3)
# create Plotter and add our glyphs with some nontrivial lighting
plotter = pv.Plotter(window_size=(1000, 800))
plotter.add_mesh(glyphs, specular=1, specular_power=15)
plotter.show()
if __name__ == "__main__":
example()
The multiglyph function in the above is mostly the same as mesh.glyph, but I've replaced the geom keyword with two keywords, geom_datasets and geom_values. These define an index -> geometry mapping that is then used to look up each glyph based on array scalars.
You asked whether you can colour the glyphs independently: you can. In the above proof of concept the choice of glyph is tied to the scalars (choosing vectors would be equally easy; I'm not so sure about arbitrary arrays). However you can easily choose what arrays to colour by when you call pv.Plotter.add_mesh, so my suggestion is to use something other than the proper scalars to colour your glyphs.
Here's a typical output:
I kept the scalars for colouring to make it easier to see the differences between the glyphs. You can see that there are five different kinds of glyphs being chosen randomly based on the random scalars. If you set non-integer scalars it will still work; I suspect vtk chooses the closest scalar or something similar for lookup.
i have been trying to create a function (Pmotion in the code below) that with several parameters gives me real and imaginary parts of the equation(that part is ok)
but in the next step i want to run the function for an increasing variable(in this case time(t) going up in jumps of 0.1 all the way to 2) and be able to plot the all these samples in an plot of the real part(Up_real in the y axis) and t in the x axis
how can i get to increase while still retaining the possibility of an initial t input?
any help would be amazing
def Pmotion(x,t,A,alpha,f):
w=2*np.pi*f
k1 = (w/alpha)
theta = k1*x-w*t
Up = k1*A*complex(-np.sin(theta),np.cos(theta))
Up_real = Up.real
Up_imag = Up.imag
plt.plot([t],[UP_real]) #here i want these to be in the x and y axis
plt.show()
#Pmotion(x=0,t=0,A=1,alpha=6000,f=2)
First of all, divide your code in small independent blocks (high cohesion) as such create a function with the desired calculation:
def Pmotion(x,t,A,alpha,f):
w=2*np.pi*f
k1 = (w/alpha)
theta = k1*x-w*t
Up = k1*A*complex(-np.sin(theta),np.cos(theta))
Up_real = Up.real
Up_imag = Up.imag
return Up_real, Up_imag
Then you can begin to think of a plotting method. e.g.
def plot_Pmotion_t():
t_range = np.arange(0,2,0.1)
reals = [Pmotion(0,t,1,6000,2) for t in t_range]
plt.plot(t_range, reals)
plt.show()
You can now freely alter or add inputs to the plot function without changing the Pmotion function.
Note: You are now plotting both real and imaginary values, change it to reals = [Pmotion(0,t,1,6000,2)[0] for t in t_range]
to only plot the real part.
Hope this helps!
In my model, I need to obtain the value of my deterministic variable from a set of parent variables using a complicated python function.
Is it possible to do that?
Following is a pyMC3 code which shows what I am trying to do in a simplified case.
import numpy as np
import pymc as pm
#Predefine values on two parameter Grid (x,w) for a set of i values (1,2,3)
idata = np.array([1,2,3])
size= 20
gridlength = size*size
Grid = np.empty((gridlength,2+len(idata)))
for x in range(size):
for w in range(size):
# A silly version of my real model evaluated on grid.
Grid[x*size+w,:]= np.array([x,w]+[(x**i + w**i) for i in idata])
# A function to find the nearest value in Grid and return its product with third variable z
def FindFromGrid(x,w,z):
return Grid[int(x)*size+int(w),2:] * z
#Generate fake Y data with error
yerror = np.random.normal(loc=0.0, scale=9.0, size=len(idata))
ydata = Grid[16*size+12,2:]*3.6 + yerror # ie. True x= 16, w= 12 and z= 3.6
with pm.Model() as model:
#Priors
x = pm.Uniform('x',lower=0,upper= size)
w = pm.Uniform('w',lower=0,upper =size)
z = pm.Uniform('z',lower=-5,upper =10)
#Expected value
y_hat = pm.Deterministic('y_hat',FindFromGrid(x,w,z))
#Data likelihood
ysigmas = np.ones(len(idata))*9.0
y_like = pm.Normal('y_like',mu= y_hat, sd=ysigmas, observed=ydata)
# Inference...
start = pm.find_MAP() # Find starting value by optimization
step = pm.NUTS(state=start) # Instantiate MCMC sampling algorithm
trace = pm.sample(1000, step, start=start, progressbar=False) # draw 1000 posterior samples using NUTS sampling
print('The trace plot')
fig = pm.traceplot(trace, lines={'x': 16, 'w': 12, 'z':3.6})
fig.show()
When I run this code, I get error at the y_hat stage, because the int() function inside the FindFromGrid(x,w,z) function needs integer not FreeRV.
Finding y_hat from a pre calculated grid is important because my real model for y_hat does not have an analytical form to express.
I have earlier tried to use OpenBUGS, but I found out here it is not possible to do this in OpenBUGS. Is it possible in PyMC ?
Update
Based on an example in pyMC github page, I found I need to add the following decorator to my FindFromGrid(x,w,z) function.
#pm.theano.compile.ops.as_op(itypes=[t.dscalar, t.dscalar, t.dscalar],otypes=[t.dvector])
This seems to solve the above mentioned issue. But I cannot use NUTS sampler anymore since it needs gradient.
Metropolis seems to be not converging.
Which step method should I use in a scenario like this?
You found the correct solution with as_op.
Regarding the convergence: Are you using pm.Metropolis() instead of pm.NUTS() by any chance? One reason this could not converge is that Metropolis() by default samples in the joint space while often Gibbs within Metropolis is more effective (and this was the default in pymc2). Having said that, I just merged this: https://github.com/pymc-devs/pymc/pull/587 which changes the default behavior of the Metropolis and Slice sampler to be non-blocked by default (so within Gibbs). Other samplers like NUTS that are primarily designed to sample the joint space still default to blocked. You can always explicitly set this with the kwarg blocked=True.
Anyway, update pymc with the most recent master and see if convergence improves. If not, try the Slice sampler.