Skip to content

Commit 5fbac25

Browse files
committed
add montecarlo integrate api
1 parent 70e5e8b commit 5fbac25

File tree

4 files changed

+239
-0
lines changed

4 files changed

+239
-0
lines changed

docs/zh/api/experimental.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@
1515
- fractional_diff
1616
- gaussian_integrate
1717
- trapezoid_integrate
18+
- montecarlo_integrate
1819
show_root_heading: true
1920
heading_level: 3

ppsci/experimental/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from ppsci.experimental.math_module import bessel_i1e
2323
from ppsci.experimental.math_module import fractional_diff
2424
from ppsci.experimental.math_module import gaussian_integrate
25+
from ppsci.experimental.math_module import montecarlo_integrate
2526
from ppsci.experimental.math_module import trapezoid_integrate
2627

2728
__all__ = [
@@ -32,4 +33,5 @@
3233
"fractional_diff",
3334
"gaussian_integrate",
3435
"trapezoid_integrate",
36+
"montecarlo_integrate",
3537
]

ppsci/experimental/math_module.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from typing import Any
1919
from typing import Callable
2020
from typing import List
21+
from typing import Optional
2122
from typing import Tuple
2223

2324
import numpy as np
@@ -462,3 +463,183 @@ def trapezoid_integrate(
462463
return paddle.cumulative_trapezoid(y, x, dx, axis)
463464
else:
464465
raise ValueError(f'mode should be "sum" or "cumsum", but got {mode}')
466+
467+
468+
def montecarlo_integrate(
469+
fn: callable,
470+
dim: int,
471+
N: int = 1000,
472+
integration_domain: Optional[List[List[float]], paddle.Tensor] = None,
473+
seed: int = None,
474+
):
475+
"""Integrates the passed function on the passed domain using vanilla Monte
476+
Carlo Integration.
477+
478+
Args:
479+
fn (func): The function to integrate over.
480+
dim (int): Dimensionality of the function's domain over which to
481+
integrate.
482+
N (int, optional): Number of sample points to use for the integration.
483+
Defaults to 1000.
484+
integration_domain (list or backend tensor, optional): Integration
485+
domain, e.g. [[-1,1],[0,1]]. Defaults to [-1,1]^dim. It can also
486+
determine the numerical backend.
487+
seed (int, optional): Random number generation seed to the sampling
488+
point creation, only set if provided. Defaults to None.
489+
490+
Raises:
491+
ValueError: If len(integration_domain) != dim
492+
493+
Returns:
494+
Integral value
495+
496+
Examples:
497+
>>> import paddle
498+
>>> import ppsci
499+
500+
>>> paddle.seed(1024)
501+
<paddle.base.libpaddle.Generator object at 0x7f27b0ef39b0>
502+
>>> # The function we want to integrate, in this example
503+
>>> # f(x0,x1) = sin(x0) + e^x1 for x0=[0,1] and x1=[-1,1]
504+
>>> # Note that the function needs to support multiple evaluations at once (first
505+
>>> # dimension of x here)
506+
>>> # Expected result here is ~3.2698
507+
>>> def some_function(x):
508+
>>> return paddle.sin(x[:, 0]) + paddle.exp(x[:, 1])
509+
510+
>>> # Compute the function integral by sampling 10000 points over domain
511+
>>> integral_value = ppsci.experimental.montecarlo_integrate(
512+
>>> some_function,
513+
>>> dim=2,
514+
>>> N=10000,
515+
>>> integration_domain=[[0, 1], [-1, 1]]
516+
>>> )
517+
518+
>>> print(integral_value)
519+
Tensor(shape=[], dtype=float32, place=Place(gpu:0), stop_gradient=True, 3.25152588)
520+
"""
521+
522+
@expand_func_values_and_squeeze_integral
523+
def calculate_result(function_values, integration_domain):
524+
"""Calculate an integral result from the function evaluations
525+
526+
Args:
527+
function_values (backend tensor): Output of the integrand
528+
integration_domain (backend tensor): Integration domain
529+
530+
Returns:
531+
backend tensor: Quadrature result
532+
"""
533+
scales = integration_domain[:, 1] - integration_domain[:, 0]
534+
volume = paddle.prod(scales)
535+
536+
# Integral = V / N * sum(func values)
537+
N = function_values.shape[0]
538+
integral = volume * paddle.sum(function_values, axis=0) / N
539+
return integral
540+
541+
def calculate_sample_points(
542+
N: int, integration_domain: paddle.Tensor, seed: int = None
543+
):
544+
"""Calculate random points for the integrand evaluation
545+
546+
Args:
547+
N (int): Number of points
548+
integration_domain (backend tensor): Integration domain
549+
seed (int, optional): Random number generation seed for the sampling point creation, only set if provided. Defaults to None.
550+
Returns:
551+
Sample points
552+
"""
553+
dim = integration_domain.shape[0]
554+
domain_starts = integration_domain[:, 0]
555+
domain_sizes = integration_domain[:, 1] - domain_starts
556+
# Scale and translate random numbers via broadcasting
557+
return (
558+
paddle.uniform(
559+
shape=[N, dim],
560+
dtype=domain_sizes.dtype,
561+
min=0.0,
562+
max=1.0,
563+
seed=seed or 0,
564+
)
565+
* domain_sizes
566+
+ domain_starts
567+
)
568+
569+
if dim is not None:
570+
if dim < 1:
571+
raise ValueError("Dimension needs to be 1 or larger.")
572+
if N is not None:
573+
if N < 1 or type(N) is not int:
574+
raise ValueError("N has to be a positive integer.")
575+
576+
integration_domain = _setup_integration_domain(dim, integration_domain)
577+
sample_points = calculate_sample_points(N, integration_domain, seed)
578+
function_values, _ = evaluate_integrand(fn, sample_points)
579+
return calculate_result(function_values, integration_domain)
580+
581+
582+
def _setup_integration_domain(
583+
dim: int, integration_domain: Optional[List[List[float]], paddle.Tensor]
584+
) -> paddle.Tensor:
585+
"""Sets up the integration domain if unspecified by the user.
586+
Args:
587+
dim (int): Dimensionality of the integration domain.
588+
integration_domain (List or Tensor): Integration domain, e.g. [[-1,1],[0,1]]. Defaults to [-1,1]^dim.
589+
Returns:
590+
Integration domain.
591+
"""
592+
# If no integration_domain is specified, create [-1,1]^d bounds
593+
if integration_domain is None:
594+
integration_domain = [[-1.0, 1.0]] * dim
595+
596+
integration_domain = [[float(b) for b in bounds] for bounds in integration_domain]
597+
598+
integration_domain = paddle.to_tensor(integration_domain)
599+
600+
if tuple(integration_domain.shape) != (dim, 2):
601+
raise ValueError(
602+
"The integration domain has an unexpected shape. "
603+
f"Expected {(dim, 2)}, got {integration_domain.shape}"
604+
)
605+
return integration_domain
606+
607+
608+
def evaluate_integrand(fn, points, weights=None, args=None):
609+
"""Evaluate the integrand function at the passed points
610+
611+
Args:
612+
fn (function): Integrand function
613+
points (backend tensor): Integration points
614+
weights (backend tensor, optional): Integration weights. Defaults to None.
615+
args (list or tuple, optional): Any arguments required by the function. Defaults to None.
616+
617+
Returns:
618+
backend tensor: Integrand function output
619+
int: Number of evaluated points
620+
"""
621+
num_points = points.shape[0]
622+
623+
if args is None:
624+
args = ()
625+
626+
result = fn(points, *args)
627+
num_results = result.shape[0]
628+
if num_results != num_points:
629+
raise ValueError(
630+
f"The passed function was given {num_points} points but only returned {num_results} value(s)."
631+
f"Please ensure that your function is vectorized, i.e. can be called with multiple evaluation points at once. It should return a tensor "
632+
f"where first dimension matches length of passed elements. "
633+
)
634+
635+
if weights is not None:
636+
if (
637+
len(result.shape) > 1
638+
): # if the the integrand is multi-dimensional, we need to reshape/repeat weights so they can be broadcast in the *=
639+
integrand_shape = paddle.to_tensor(result.shape[1:])
640+
weights = paddle.repeat(
641+
paddle.unsqueeze(weights, axis=1), paddle.prod(integrand_shape)
642+
).reshape((weights.shape[0], *(integrand_shape)))
643+
result *= weights
644+
645+
return result, num_points
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
2+
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from typing import Callable
16+
from typing import List
17+
18+
import numpy as np
19+
import paddle
20+
import pytest
21+
22+
import ppsci
23+
24+
paddle.seed(1024)
25+
26+
27+
@pytest.mark.parametrize(
28+
"fn, dim, N, integration_domains, expected",
29+
[
30+
(
31+
lambda x: paddle.sin(x[:, 0]) + paddle.exp(x[:, 1]),
32+
2,
33+
10000,
34+
[[0, 1], [-1, 1]],
35+
3.25152588,
36+
)
37+
],
38+
)
39+
def test_montecarlo_integrate(
40+
fn: Callable,
41+
dim: int,
42+
N: int,
43+
integration_domains: List[List[float]],
44+
expected: float,
45+
):
46+
assert np.allclose(
47+
ppsci.experimental.montecarlo_integrate(
48+
fn, dim, N, integration_domains
49+
).numpy(),
50+
expected,
51+
)
52+
53+
54+
if __name__ == "__main__":
55+
pytest.main()

0 commit comments

Comments
 (0)