warwick-sciml 2026.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ["v*"]
7
+ pull_request:
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: astral-sh/setup-uv@v6
15
+ - run: uv run --python 3.12 --extra test pytest -q
16
+
17
+ publish:
18
+ # Publish to PyPI when a version tag (e.g. v2026.1.0) is pushed, via trusted publishing
19
+ if: startsWith(github.ref, 'refs/tags/v')
20
+ needs: test
21
+ runs-on: ubuntu-latest
22
+ environment: pypi
23
+ permissions:
24
+ id-token: write
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: astral-sh/setup-uv@v6
28
+ - name: Check tag matches package version
29
+ run: test "v$(uv version --short)" = "${GITHUB_REF_NAME}"
30
+ - run: uv build
31
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ .venv/
4
+ dist/
5
+ .pytest_cache/
6
+ *.log
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 James Kermode
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.5
2
+ Name: warwick-sciml
3
+ Version: 2026.1.0
4
+ Summary: Support modules for the Warwick PX914/ES98E Predictive Modelling, UQ and Scientific Machine Learning module
5
+ Project-URL: Repository, https://github.com/HetSys/warwick-sciml
6
+ Author: James Kermode
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.11
10
+ Requires-Dist: matplotlib
11
+ Requires-Dist: numpy
12
+ Requires-Dist: pandas
13
+ Requires-Dist: scipy
14
+ Requires-Dist: tqdm
15
+ Provides-Extra: atomistic
16
+ Requires-Dist: ase; extra == 'atomistic'
17
+ Requires-Dist: atomistica; extra == 'atomistic'
18
+ Requires-Dist: jax; extra == 'atomistic'
19
+ Requires-Dist: matscipy; extra == 'atomistic'
20
+ Provides-Extra: gp
21
+ Requires-Dist: jax; extra == 'gp'
22
+ Requires-Dist: tinygp; extra == 'gp'
23
+ Provides-Extra: test
24
+ Requires-Dist: ase; extra == 'test'
25
+ Requires-Dist: atomistica; extra == 'test'
26
+ Requires-Dist: jax; extra == 'test'
27
+ Requires-Dist: matscipy; extra == 'test'
28
+ Requires-Dist: pytest; extra == 'test'
29
+ Requires-Dist: tinygp; extra == 'test'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # warwick-sciml
33
+
34
+ Support modules for the University of Warwick module on Predictive Modelling,
35
+ Uncertainty Quantification and Scientific Machine Learning (PX914 / ES98E).
36
+
37
+ ```bash
38
+ pip install warwick-sciml # core: BLR, sensitivity analysis, fluid/FE solvers
39
+ pip install "warwick-sciml[gp]" # + Gaussian process utilities (JAX, tinygp)
40
+ pip install "warwick-sciml[atomistic]" # + atomistic examples (ASE, atomistica, matscipy, JAX)
41
+ ```
42
+
43
+ In a marimo notebook, add it to the PEP 723 header instead, e.g.
44
+
45
+ ```python
46
+ # /// script
47
+ # dependencies = ["warwick-sciml[atomistic]>=2026.1", "marimo>=0.25"]
48
+ # ///
49
+ ```
50
+
51
+ then `from sciml.blr import design_matrix`, `from sciml import tersoff_jax`, etc.
52
+
53
+ | Module | Contents |
54
+ |---|---|
55
+ | `sciml.blr` | Bayesian linear regression: basis functions, design matrices, posteriors |
56
+ | `sciml.sensitivity` | finite-difference sensitivities and Taylor expansions |
57
+ | `sciml.fluidsolvers` | Poiseuille flow solvers |
58
+ | `sciml.femsolvers` | 2D plane-stress finite element solver |
59
+ | `sciml.gputils` | Gaussian process fitting and plotting (extra `gp`) |
60
+ | `sciml.atomistic` | Tersoff calculators, vacancy formation energies (extra `atomistic`) |
61
+ | `sciml.tersoff_jax` | differentiable Tersoff potential in JAX (extra `atomistic`) |
62
+ | `sciml.progress` | tqdm wrappers for lecture notebooks |
63
+
64
+ Versions follow the academic year (`2026.x` for 2026/27).
65
+
66
+ ## Development
67
+
68
+ ```bash
69
+ uv run --extra test pytest
70
+ ```
@@ -0,0 +1,39 @@
1
+ # warwick-sciml
2
+
3
+ Support modules for the University of Warwick module on Predictive Modelling,
4
+ Uncertainty Quantification and Scientific Machine Learning (PX914 / ES98E).
5
+
6
+ ```bash
7
+ pip install warwick-sciml # core: BLR, sensitivity analysis, fluid/FE solvers
8
+ pip install "warwick-sciml[gp]" # + Gaussian process utilities (JAX, tinygp)
9
+ pip install "warwick-sciml[atomistic]" # + atomistic examples (ASE, atomistica, matscipy, JAX)
10
+ ```
11
+
12
+ In a marimo notebook, add it to the PEP 723 header instead, e.g.
13
+
14
+ ```python
15
+ # /// script
16
+ # dependencies = ["warwick-sciml[atomistic]>=2026.1", "marimo>=0.25"]
17
+ # ///
18
+ ```
19
+
20
+ then `from sciml.blr import design_matrix`, `from sciml import tersoff_jax`, etc.
21
+
22
+ | Module | Contents |
23
+ |---|---|
24
+ | `sciml.blr` | Bayesian linear regression: basis functions, design matrices, posteriors |
25
+ | `sciml.sensitivity` | finite-difference sensitivities and Taylor expansions |
26
+ | `sciml.fluidsolvers` | Poiseuille flow solvers |
27
+ | `sciml.femsolvers` | 2D plane-stress finite element solver |
28
+ | `sciml.gputils` | Gaussian process fitting and plotting (extra `gp`) |
29
+ | `sciml.atomistic` | Tersoff calculators, vacancy formation energies (extra `atomistic`) |
30
+ | `sciml.tersoff_jax` | differentiable Tersoff potential in JAX (extra `atomistic`) |
31
+ | `sciml.progress` | tqdm wrappers for lecture notebooks |
32
+
33
+ Versions follow the academic year (`2026.x` for 2026/27).
34
+
35
+ ## Development
36
+
37
+ ```bash
38
+ uv run --extra test pytest
39
+ ```
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "warwick-sciml"
7
+ version = "2026.1.0"
8
+ description = "Support modules for the Warwick PX914/ES98E Predictive Modelling, UQ and Scientific Machine Learning module"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "James Kermode" }]
14
+
15
+ dependencies = [
16
+ "numpy",
17
+ "scipy",
18
+ "matplotlib",
19
+ "pandas",
20
+ "tqdm",
21
+ ]
22
+
23
+ [project.urls]
24
+ Repository = "https://github.com/HetSys/warwick-sciml"
25
+
26
+ [project.optional-dependencies]
27
+ # Gaussian process utilities (sciml.gputils)
28
+ gp = ["jax", "tinygp"]
29
+ # Atomistic examples (sciml.atomistic, sciml.tersoff_jax)
30
+ atomistic = ["ase", "atomistica", "matscipy", "jax"]
31
+ test = ["pytest", "warwick-sciml[gp,atomistic]"]
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/sciml"]
35
+
36
+ [tool.pytest.ini_options]
37
+ filterwarnings = ["ignore::DeprecationWarning"]
@@ -0,0 +1,16 @@
1
+ """Support modules for the Warwick PX914/ES98E module on Predictive Modelling,
2
+ Uncertainty Quantification and Scientific Machine Learning.
3
+
4
+ Submodules (import them explicitly, e.g. ``from sciml.blr import design_matrix``):
5
+
6
+ - ``sciml.blr`` Bayesian linear regression: basis functions, design matrices, posteriors
7
+ - ``sciml.sensitivity`` finite-difference sensitivities and Taylor expansions
8
+ - ``sciml.fluidsolvers`` Poiseuille flow solvers
9
+ - ``sciml.femsolvers`` 2D plane-stress finite element solver
10
+ - ``sciml.gputils`` Gaussian process fitting and plotting with JAX/tinygp [extra: gp]
11
+ - ``sciml.atomistic`` Tersoff calculators and vacancy formation energies [extra: atomistic]
12
+ - ``sciml.tersoff_jax`` differentiable Tersoff potential in JAX [extra: atomistic]
13
+ - ``sciml.progress`` tqdm wrappers suited to lecture notebooks
14
+ """
15
+
16
+ __version__ = "2026.1.0"
@@ -0,0 +1,88 @@
1
+ import sys
2
+
3
+ import numpy as np
4
+
5
+ from ase.build import bulk
6
+ from ase.optimize.precon import PreconLBFGS
7
+ from atomistica import Tersoff, Tersoff_PRB_39_5566_Si_C
8
+
9
+ Tersoff_parameter_names = list(Tersoff_PRB_39_5566_Si_C.keys())[2:]
10
+ del Tersoff_parameter_names[Tersoff_parameter_names.index('m')]
11
+ del Tersoff_parameter_names[Tersoff_parameter_names.index('mubo')]
12
+
13
+ Tersoff_parameter_values = np.array([Tersoff_PRB_39_5566_Si_C[key][-1] for key in Tersoff_parameter_names])
14
+
15
+ def tersoff_calculator(parameters):
16
+ # build a dictionary of parameters using values passed in
17
+ param_dict = { 'el': 'Si', 'm': 1 } # fixed parameters
18
+ param_dict.update(dict(zip(Tersoff_parameter_names, parameters)))
19
+ calc = Tersoff(**param_dict)
20
+ return calc
21
+
22
+ def vacancy_cell(Ncell=3, rattle=0.0):
23
+ """
24
+ Build an Ncell x Ncell x Ncell cubic Si supercell and a copy with a vacancy.
25
+
26
+ The atom nearest the cell midpoint is removed. Positions in the vacancy cell
27
+ are optionally rattled (standard deviation `rattle`, in Angstrom) before the
28
+ atom is deleted.
29
+
30
+ Returns
31
+ -------
32
+ at_sup: perfect supercell
33
+ at_vac: supercell with the vacancy
34
+ vac_pos: position of the removed atom
35
+ """
36
+ at_sup = bulk('Si', cubic=True) * Ncell
37
+ at_vac = at_sup.copy()
38
+ if rattle:
39
+ at_vac.rattle(rattle)
40
+ cell_midpoint = np.diag(at_vac.cell) * 0.5
41
+ vac_atom = ((at_vac.positions - cell_midpoint)**2).sum(axis=1).argmin()
42
+ vac_pos = at_vac.positions[vac_atom].copy()
43
+ del at_vac[vac_atom]
44
+ return at_sup, at_vac, vac_pos
45
+
46
+
47
+ def vacancy_formation_energy(parameters, Ncell=3, relax=False, fmax=1e-4, logfile=None):
48
+ """
49
+ Compute vacancy formation energy for a Ncell x Ncell x Ncell Si system
50
+
51
+ Modified to use given values of the Tersoff parameters
52
+
53
+ Arguments
54
+ ---------
55
+
56
+ parameters: vector of the Tersoff parameters to be used
57
+ Ncell: number of unit cells (default 3)
58
+ relax: if true, rattle the atoms slightly and perform a geometry
59
+ optimisation before computing the vacancy formation energy
60
+ fmax: the maximum force tolerance to use when `relax=True`
61
+
62
+ Returns
63
+ -------
64
+
65
+ e_vac Vacancy formation energy, in eV
66
+
67
+ """
68
+ at_sup, at_vac, _ = vacancy_cell(Ncell, rattle=0.01 if relax else 0.0)
69
+
70
+ calc = tersoff_calculator(parameters)
71
+ at_sup.calc = calc
72
+ e_bulk_per_atom = at_sup.get_potential_energy() / len(at_sup)
73
+
74
+ at_vac.calc = calc
75
+ if relax:
76
+ opt = PreconLBFGS(at_vac, logfile=logfile)
77
+ opt.run(fmax=fmax)
78
+ return at_vac.get_potential_energy() - e_bulk_per_atom * len(at_vac)
79
+
80
+
81
+ def vacancy_formation_energy_first5(x):
82
+ X = Tersoff_parameter_values.copy()
83
+ X[:5] = x
84
+ _stdout = sys.stdout
85
+ sys.stdout = None # suppress output
86
+ Ef = vacancy_formation_energy(X, relax=True)
87
+ sys.stdout = _stdout
88
+ return Ef
@@ -0,0 +1,133 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+
4
+
5
+ def design_matrix(X, phi):
6
+ """
7
+ Arguments:
8
+
9
+ X - The observed inputs
10
+ phi - The basis functions
11
+ """
12
+ num_observations = X.shape[0]
13
+ num_basis = phi.num_basis
14
+ Phi = np.zeros((num_observations, num_basis))
15
+ for i in range(num_observations):
16
+ Phi[i, :] = phi(X[i, :])
17
+ return Phi
18
+
19
+
20
+ class LinearBasis:
21
+ """
22
+ Represents a 1D linear basis.
23
+ """
24
+
25
+ def __init__(self):
26
+ self.num_basis = 2 # The number of basis functions
27
+
28
+ def __call__(self, x):
29
+ """
30
+ ``x`` should be a 1D array of inputs
31
+ """
32
+ return [1.0, x[0]]
33
+
34
+
35
+ class PolynomialBasis:
36
+ """
37
+ A set of polynomial basis functions.
38
+
39
+ Arguments:
40
+ degree - The degree of the polynomial.
41
+ """
42
+
43
+ def __init__(self, degree):
44
+ self.degree = degree
45
+ self.num_basis = degree + 1
46
+
47
+ def __call__(self, x):
48
+ return np.array([x[0] ** i for i in range(self.degree + 1)])
49
+
50
+
51
+ class RadialBasisFunctions:
52
+ """
53
+ A set of linear basis functions.
54
+
55
+ Arguments:
56
+ X - The centers of the radial basis functions.
57
+ ell - The assumed lengthscale.
58
+ """
59
+
60
+ def __init__(self, X, ell):
61
+ self.X = X
62
+ self.ell = ell
63
+ self.num_basis = X.shape[0]
64
+
65
+ def __call__(self, x):
66
+ return np.exp(-0.5 * (x - self.X) ** 2 / self.ell**2).flatten()
67
+
68
+
69
+ def least_squares_MLE(X, y):
70
+ """Compute maximum likelihood estimate of mean and standard deviation of weights"""
71
+ w_MLE, res_MLE, _, _ = np.linalg.lstsq(X, y, rcond=None)
72
+ sigma_MLE = np.sqrt(res_MLE / X.shape[0])
73
+ return w_MLE, sigma_MLE
74
+
75
+
76
+ def prior(alpha, N):
77
+ """Compute mean and covariance matrices of the weight prior"""
78
+ m0 = np.zeros(N)
79
+ S0 = 1.0 / alpha * np.eye(N)
80
+ return m0, S0
81
+
82
+
83
+ def posterior(Phi, y, alpha, beta, return_inverse=False):
84
+ """Computes mean and covariance matrix of the posterior distribution."""
85
+ S_N_inv = alpha * np.eye(Phi.shape[1]) + beta * Phi.T.dot(Phi)
86
+ S_N = np.linalg.inv(S_N_inv)
87
+ m_N = beta * S_N.dot(Phi.T).dot(y)
88
+ m_N = m_N
89
+ if return_inverse:
90
+ return m_N, S_N, S_N_inv
91
+ else:
92
+ return m_N, S_N
93
+
94
+
95
+ def posterior_predictive(Phi_test, m_N, S_N, beta):
96
+ """Computes mean and variances of the posterior predictive distribution."""
97
+ y = Phi_test.dot(m_N).ravel()
98
+ # Only compute variances (diagonal elements of covariance matrix)
99
+ y_epi = np.sum(Phi_test.dot(S_N) * Phi_test, axis=1)
100
+ y_var = 1 / beta + y_epi
101
+ return y, y_epi, y_var
102
+
103
+
104
+ def plot_data(X, y):
105
+ plt.plot(X[:, 0], y[:, 0], "kx", ms=10)
106
+
107
+
108
+ def plot_truth(X, y, label="Truth"):
109
+ plt.plot(X[:, 0], y[:, 0], "k--", label=label)
110
+
111
+
112
+ def plot_posterior_samples(X, y):
113
+ plt.plot(X, y, "r-")
114
+ plt.axis("equal")
115
+
116
+
117
+ def plot_predictive(X, y, y_epi, y_var):
118
+ sigma_epi = np.sqrt(y_epi) # epistemitic uncertainty
119
+ sigma_tot = np.sqrt(y_var) # total uncertainty
120
+
121
+ y_el = y - 2 * sigma_epi
122
+ y_tl = y - 2 * sigma_tot
123
+ y_eu = y + 2 * sigma_epi
124
+ y_tu = y + 2 * sigma_tot
125
+
126
+ plt.plot(X[:, 0], y, "b-", label="Prediction")
127
+ plt.fill_between(
128
+ X[:, 0], y_el, y_eu, color="C2", label="Epistemic uncertainty", alpha=0.3
129
+ )
130
+ plt.fill_between(
131
+ X[:, 0], y_tl, y_el, color="C1", label="Total uncertainty", alpha=0.3
132
+ )
133
+ plt.fill_between(X[:, 0], y_eu, y_tu, color="C1", alpha=0.3)
@@ -0,0 +1,118 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+
4
+ def plot_mesh(XYZ, c='k', ls='--'):
5
+ wrap = list(range(len(XYZ))) + [0]
6
+ plt.plot(XYZ[wrap, 0], XYZ[wrap, 1], 's'+c+ls)
7
+ plt.xlabel("$x_1$")
8
+ # Set y axis label.
9
+ plt.ylabel("$x_2$")
10
+
11
+ def plane_stress(E,nu):
12
+ C = np.zeros((3,3))
13
+ De = np.array([[1,nu,0],
14
+ [nu,1,0], [0,0,(1-nu)/2]])
15
+ De1 = E/(1-nu**2)
16
+ C = np.dot(De1,De)
17
+ return C
18
+
19
+ def dispstrain_B(xyze, xi, eta):
20
+ natcoord = np.array([[-1, 1, 1, -1],[-1, -1, 1, 1]]) #natural nodal coordinates of a quad element
21
+
22
+ # derivatives of shape functions w.r.t. natural coordinates
23
+ dNdnat = np.zeros((2,4))
24
+ dNdnat[0,:]=(1/4)*natcoord[0,:]*(1+natcoord[1,:]*eta)
25
+ dNdnat[1,:]=(1/4)*natcoord[1,:]*(1+natcoord[0,:]*xi)
26
+
27
+ # element Jacobian matrix
28
+ Jmat = np.dot(dNdnat,xyze) # [2x2 matrix]
29
+ J = np.linalg.det(Jmat) # determinant of the Jacobian
30
+
31
+ JmatInv = np.linalg.inv(Jmat) # inverse of the Jacobian matrix
32
+ dNdx = np.dot(JmatInv, dNdnat) # effectively: Jmat^-1 * dNdna
33
+
34
+ #displacement-strain matrix
35
+ #linear QUAD element
36
+ dsB = np.zeros((3,8)) # [3 strain components X 8 DOFs]
37
+ dsB[0, 0::2] = dNdx[0, :]
38
+ dsB[1, 1::2] = dNdx[1, :]
39
+ dsB[2, 0::2] = dNdx[1, :]
40
+ dsB[2, 1::2] = dNdx[0, :]
41
+
42
+ return dsB, J
43
+
44
+ def keval(xyze, Ce, th):
45
+ ke=np.zeros((8,8)) # create element stiffness matrix (ndof x ndof)
46
+
47
+ a = 1/(np.sqrt(3)) # location of Gauss points (in natural coordinates)
48
+ w = 1 # weights
49
+ Gauss = np.array([[-a, a, a, -a],[-a, -a, a, a]]) # Gauss points matrix
50
+
51
+ for i in range(4): # introduce natural coordinates
52
+ xi=Gauss[0,i] # natural coordinate - horizontal
53
+ eta=Gauss[1,i] # natural coordinate - vertical
54
+
55
+ dsB, J = dispstrain_B(xyze,xi,eta) # evaluate dsB matrix and Jacobian
56
+
57
+ dsBT=dsB.transpose()
58
+ dot1=np.dot(dsBT,Ce)
59
+ dot2=np.dot(dot1,dsB)
60
+ ke=ke+dot2*J*th*w # evaluate element stiffness matrix
61
+
62
+ return ke
63
+
64
+ def sigma_vector(XYZ, C, de):
65
+ a=1/np.sqrt(3) # location of Gauss points (in parent coordinates)
66
+ w=1 # weights
67
+ Gauss = np.array([[-a, a, a, -a],[-a, -a, a, a]]) # Gauss points matrix
68
+ sigma = np.zeros(3) # stress (Voigt notation)
69
+
70
+ for j in range(4): # loop over element integration points
71
+ xi=Gauss[0,j] # natural coordinate - horizontal
72
+ eta=Gauss[1,j] # natural coordinate - vertical
73
+ dsB, J = dispstrain_B(XYZ,xi,eta) # evaluate dsB matrix and Jacobian
74
+ sG1 = np.dot(dsB,de) # strain vector (Voigt notation)
75
+ sG2 = sG1
76
+ sigmaGauss = np.dot(C, sG2) # stresses at j-th Gauss point within parent element
77
+ sigma = sigma + sigmaGauss*J*w # sum of the stresses over all Gauss points within physical element
78
+
79
+ return sigma
80
+
81
+ def quad_solver(x, plot=False):
82
+ """
83
+ Wrapper around simple linear FE solver for trapezoidal geometry from PX912
84
+
85
+ Arguments:
86
+ x - vector containing [E (Pa), nu (dimensionless), t (m), fext_top (N)]
87
+ plot - if True, plot mesh before and after applying load
88
+
89
+ Returns:
90
+ stress - array of length 3 containing [sigma_xx, sigma_yy, sigma_xy] in units of Pa
91
+ """
92
+ E, nu, th, fext_top = x # unpack inputs
93
+
94
+ XYZ = np.array([[0,1],[0,0],[2,0.5],[2,1]]) # nodal coordinates
95
+
96
+ ndof = XYZ.size
97
+ BC = np.ones(ndof, dtype=int)
98
+
99
+ BC[[0, 1, 2, 3]] = 0
100
+ BCid = BC.nonzero()[0]
101
+
102
+ # Applied loads - define the RHS load vector
103
+ rhs = np.zeros(ndof)
104
+ rhs[[1, 7]] = fext_top
105
+ rhs = rhs[BCid]
106
+
107
+ C = plane_stress(E,nu)
108
+ ke = keval(XYZ, C, th)
109
+ ke = ke[np.ix_(BCid,BCid)]
110
+ ue = np.linalg.solve(ke, rhs)
111
+ de = np.zeros(XYZ.size)
112
+ de[BCid] = ue
113
+ if plot:
114
+ sdef=10000 # scale deformation
115
+ plot_mesh(XYZ)
116
+ plot_mesh(XYZ + sdef*de.reshape(XYZ.shape), c='r', ls='-')
117
+ sigma = sigma_vector(XYZ, C, de)
118
+ return sigma
@@ -0,0 +1,94 @@
1
+ import numpy as np
2
+ import scipy.sparse as sparse
3
+ import scipy.sparse.linalg as linalg
4
+
5
+ def poiseuille_solver_1(x, N=1000, return_all=False):
6
+ F, W = x # unpack parameters
7
+ F = F*np.ones(N) # vector for RHS
8
+ F[0] = F[-1] = 0 # enforce boundary conditions on RHS
9
+ h = 2*W/(N-1)
10
+ A = sparse.dia_matrix((N, N)) # assemble matrix A
11
+
12
+ # Build diagonals with boundary condition modifications
13
+ main_dia = np.ones(N) * 2/h**2
14
+ main_dia[0] = main_dia[-1] = 1 # enforce u[0] = u[-1] = 0
15
+ upper_dia = np.ones(N-1) * -1/h**2
16
+ upper_dia[0] = 0 # no coupling at left boundary
17
+ lower_dia = np.ones(N-1) * -1/h**2
18
+ lower_dia[-1] = 0 # no coupling at right boundary
19
+
20
+ A.setdiag(main_dia)
21
+ A.setdiag(upper_dia, 1)
22
+ A.setdiag(lower_dia, -1)
23
+ A = A.tocsc() # convert to CSC storage for efficient solve
24
+ u = linalg.spsolve(A, F)
25
+ Q = np.sum(u)*h
26
+ if return_all:
27
+ return u, F, Q
28
+ else:
29
+ return Q
30
+
31
+ def poiseuille_solver_2(x, N=1000, return_all=False):
32
+ F_l, F_h, W = x # unpack parameters
33
+
34
+ # build vector for F used for RHS of linear system
35
+ y = np.linspace(-W, W, N)
36
+ F = np.where((y > 0) & (y < W/2), F_l, F_h)
37
+ F[0] = F[-1] = 0 # enforce boundary conditions on RHS
38
+
39
+ h = 2*W/(N-1)
40
+ A = sparse.dia_matrix((N, N)) # assemble matrix A
41
+
42
+ # Build diagonals with boundary condition modifications
43
+ main_dia = np.ones(N) * 2/h**2
44
+ main_dia[0] = main_dia[-1] = 1 # enforce u[0] = u[-1] = 0
45
+ upper_dia = np.ones(N-1) * -1/h**2
46
+ upper_dia[0] = 0 # no coupling at left boundary
47
+ lower_dia = np.ones(N-1) * -1/h**2
48
+ lower_dia[-1] = 0 # no coupling at right boundary
49
+
50
+ A.setdiag(main_dia)
51
+ A.setdiag(upper_dia, 1)
52
+ A.setdiag(lower_dia, -1)
53
+ A = A.tocsc() # convert to CSC storage for efficient solve
54
+ u = linalg.spsolve(A, F)
55
+ Q = np.sum(u)*h
56
+
57
+ if return_all:
58
+ return u, F, Q
59
+ else:
60
+ return Q
61
+
62
+ def poiseuille_solver_3(F, W=2, return_all=False):
63
+ N = len(F)
64
+ F = F.copy() # avoid modifying input
65
+ F[0] = F[-1] = 0 # enforce boundary conditions on RHS
66
+ h = 2*W/(N-1)
67
+ A = sparse.dia_matrix((N, N)) # assemble matrix A
68
+
69
+ # Build diagonals with boundary condition modifications
70
+ main_dia = np.ones(N) * 2/h**2
71
+ main_dia[0] = main_dia[-1] = 1 # enforce u[0] = u[-1] = 0
72
+ upper_dia = np.ones(N-1) * -1/h**2
73
+ upper_dia[0] = 0 # no coupling at left boundary
74
+ lower_dia = np.ones(N-1) * -1/h**2
75
+ lower_dia[-1] = 0 # no coupling at right boundary
76
+
77
+ A.setdiag(main_dia)
78
+ A.setdiag(upper_dia, 1)
79
+ A.setdiag(lower_dia, -1)
80
+ A = A.tocsc() # convert to CSC storage for efficient solve
81
+ u = linalg.spsolve(A, F) # parameter vector specifies RHS
82
+ Q = np.sum(u)*h
83
+ if return_all:
84
+ return u, F, Q
85
+ else:
86
+ return Q
87
+
88
+
89
+ # +
90
+ def u_analytic(y, F, W):
91
+ return F / 2 * (W ** 2 - y ** 2)
92
+
93
+ def Q_analytic(F, W):
94
+ return 2 * W ** 3 * F / 3