Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sprint16 time dep refactor #182

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions cuqi/array/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from ._array import MultiVector, Samples, TimeSeries, CUQIarray2
35 changes: 35 additions & 0 deletions cuqi/array/_array.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Optional
import cuqi

__all__ = ["MultiVector", "Samples", "TimeSeries", "CUQIarray2"]

class MultiVector(ABC):
pass

class Samples(MultiVector):
pass

class TimeSeries(MultiVector):
"""
Abstract base class for time series.

times: Time points of the time series.
values: Values of the time series.
"""
pass

class TimeDependantPDESolution(TimeSeries):
"""
Abstract base class for time series of PDEs.

rejected_times: Time points of the time series that were rejected.
local_errors: Estimated local errors of the time steps.
u_list: List of solutions at the time points.
t_list: List of time steps.
"""
pass

class CUQIarray2(ABC):
pass
1 change: 1 addition & 0 deletions cuqi/forward_model_gallery/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from ._pde_gallary import Heat1D, Poisson1D, Poisson2D, Wave2D
11 changes: 11 additions & 0 deletions cuqi/forward_model_gallery/_pde_gallery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class Heat1D():
pass

class Poisson1D():
pass

class Poisson2D():
pass

class Wave2D():
pass
6 changes: 6 additions & 0 deletions cuqi/pde/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@
SteadyStateLinearPDE,
TimeDependentLinearPDE
)
from ._time_integrator import (
TimeIntegrator,
ForwardEuler,
BackwardEuler,
ThetaMethod,
Trapezoidal)
17 changes: 17 additions & 0 deletions cuqi/pde/_observer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class Observer():
pass

class SteadyObserver(Observer):
"""
Attributes:
observation_operator: Observation operator (lambda function or x coordinates).
observed_solution: Observed solution (CUQIarray)."""
pass

class TimeDependantObserver(Observer):
"""
Attributes:
observation_operator: Observation operator (lambda function or x and t coordinates).
observed_solution: Observed solution (TimeSeries or CUQIArray)."""

pass
54 changes: 17 additions & 37 deletions cuqi/pde/_pde.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from inspect import getsource
from scipy.interpolate import interp1d
import numpy as np
from ._time_integrator import ForwardEuler, BackwardEuler

class PDE(ABC):
"""
Expand Down Expand Up @@ -92,7 +93,7 @@ def grids_equal(self):
return self._grids_equal


class LinearPDE(PDE):
class LinearPDE(PDE, ABC):
"""
Parametrized Linear PDE base class

Expand Down Expand Up @@ -122,9 +123,9 @@ def __init__(self, PDE_form, linalg_solve=None, linalg_solve_kwargs=None, **kwar
self._linalg_solve = linalg_solve
self._linalg_solve_kwargs = linalg_solve_kwargs

def _solve_linear_system(self, A, b, linalg_solve, kwargs):
def _solve_linear_system(self, A, b):
"""Helper function that solves the linear system `A*x=b` using the provided solve method `linalg_solve` and its keyword arguments `kwargs`. It then returns the output in the format: `solution`, `info`"""
returned_values = linalg_solve(A, b, **kwargs)
returned_values = self._linalg_solve(A, b, **self._linalg_solve_kwargs)
if isinstance(returned_values, tuple):
solution = returned_values[0]
info = returned_values[1:]
Expand Down Expand Up @@ -162,7 +163,7 @@ def solve(self):
if not hasattr(self, "diff_op") or not hasattr(self, "rhs"):
raise Exception("PDE is not assembled.")

return self._solve_linear_system(self.diff_op, self.rhs, self._linalg_solve, self._linalg_solve_kwargs)
return self._solve_linear_system(self.diff_op, self.rhs)


def observe(self, solution):
Expand Down Expand Up @@ -199,53 +200,32 @@ class TimeDependentLinearPDE(LinearPDE):
See demos/demo34_TimeDependentLinearPDE.py for 1D heat and 1D wave equations.
"""

def __init__(self, PDE_form, time_steps, method='forward_euler', **kwargs):
def __init__(self, PDE_form, time_steps, method=ForwardEuler(), **kwargs):
super().__init__(PDE_form, **kwargs)

self.time_steps = time_steps
self.method = method

@property
def method(self):
return self._method

@method.setter
def method(self, value):
if value.lower() != 'forward_euler' and value.lower() != 'backward_euler':
raise ValueError(
"method can be set to either `forward_euler` or `backward_euler`")
self._method = value

def assemble(self, parameter):
"""Assemble PDE"""
self._parameter = parameter

def assemble_step(self, t):
def assemble_time_dependant_step(self, t):
"""Assemble time step at time t"""
self.diff_op, self.rhs, self.initial_condition = self.PDE_form(self._parameter, t)

def solve(self):
"""Solve PDE by time-stepping"""
#TODO: use time-stepping class

if self.method == 'forward_euler':
for idx, t in enumerate(self.time_steps[:-1]):
dt = self.time_steps[idx+1] - t
self.assemble_step(t)
if idx == 0:
u = self.initial_condition
u = (dt*self.diff_op + np.eye(len(u)))@u + dt*self.rhs # from u at time t, gives u at t+dt
info = None

if self.method == 'backward_euler':
for idx, t in enumerate(self.time_steps[1:]):
dt = t - self.time_steps[idx]
self.assemble_step(t)
if idx == 0:
u = self.initial_condition
A = np.eye(len(u)) - dt*self.diff_op
# from u at time t-dt, gives u at t
u, info = self._solve_linear_system(
A, u + dt*self.rhs, self._linalg_solve, self._linalg_solve_kwargs)
for idx, t in enumerate(self.time_steps[:-1]):
dt = self.time_steps[idx+1] - t
self.assemble_time_dependant_step(t)
# can do self.observe_time_dependant_step(t) here
if idx == 0:
u = self.initial_condition
u, _ = self.method.propagate(u, dt, self.rhs, self.diff_op, self._solve_linear_system)
info = None

return u, info

Expand All @@ -259,4 +239,4 @@ def observe(self, solution):
if self.observation_map is not None:
solution_obs = self.observation_map(solution_obs)

return solution_obs
return solution_obs
37 changes: 37 additions & 0 deletions cuqi/pde/_time_integrator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# do not import numpy
from __future__ import annotations
from abc import ABC, abstractmethod
import numpy as np
from typing import Optional
import cuqi

class TimeIntegrator(ABC):
@abstractmethod
def propagate(self, u, dt, rhs, diff_op, solve_linear_system, I=None, apply_bc=None):
"""Propagate the solution using the explicit Euler method."""
pass

class ThetaMethod(TimeIntegrator):
def __init__(self, theta):
# solver details
self.theta = theta # 0: backward Euler, 1: forward Euler, 0.5: trapezoidal

def propagate(self, u, dt, rhs, diff_op, solver, I=None, apply_bc=None):
if I is None:
I = np.eye(len(u))
rhs_op = I + self.theta*dt*diff_op
lhs_op = I - (1-self.theta)*dt*diff_op
u_next, info = solver(rhs_op, lhs_op@u + dt*rhs)
return u_next, info

class ForwardEuler(ThetaMethod):
def __init__(self):
super().__init__(1.0)

class BackwardEuler(ThetaMethod):
def __init__(self):
super().__init__(0.0)

class Trapezoidal(ThetaMethod):
def __init__(self):
super().__init__(0.5)
10 changes: 10 additions & 0 deletions cuqi/pde/_time_step_selector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from abc import ABC, abstractmethod

class Static():
pass

class Dynamic(ABC):
pass

class Standard(Dynamic):
pass