Metadata-Version: 2.4
Name: find-closed-form
Version: 0.5.0
Summary: Find closed-form expressions for numerical values
Author-email: Daniele Gregori <dangregori@gmail.com>
Maintainer-email: Daniele Gregori <dangregori@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/Daniele-Gregori/PyPI-packages/tree/main/packages/find-closed-form
Project-URL: Documentation, https://resources.wolframcloud.com/FunctionRepository/resources/FindClosedForm/
Project-URL: Repository, https://github.com/Daniele-Gregori/PyPI-packages/tree/main/packages/find-closed-form
Project-URL: Changelog, https://github.com/Daniele-Gregori/PyPI-packages/blob/main/packages/find-closed-form/CHANGELOG.md
Project-URL: Issues, https://github.com/Daniele-Gregori/PyPI-packages/issues
Keywords: closed-form,symbolic-computation,number-theory,mathematics,inverse-symbolic-calculator,number-recognition,symbolic-regression,wolfram
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Education
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Mathematics
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: sympy>=1.12
Requires-Dist: farey>=0.7.0
Provides-Extra: ranges
Requires-Dist: algebraic-range>=0.9.0; extra == "ranges"
Requires-Dist: transcendental-range>=0.9.0; extra == "ranges"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Dynamic: license-file

# find-closed-form

[![find-closed-form](https://github.com/Daniele-Gregori/PyPI-packages/actions/workflows/find-closed-form.yml/badge.svg)](https://github.com/Daniele-Gregori/PyPI-packages/actions/workflows/find-closed-form.yml)
[![PyPI version](https://badge.fury.io/py/find-closed-form.svg)](https://badge.fury.io/py/find-closed-form)
[![Python](https://img.shields.io/pypi/pyversions/find-closed-form)](https://pypi.org/project/find-closed-form/)

A Python port of the Wolfram Language resource function
[FindClosedForm](https://resources.wolframcloud.com/FunctionRepository/resources/FindClosedForm/),
contributed by the same author.

`find_closed_form` helps solve the fundamental problem of
[number recognition](https://reference.wolfram.com/language/guide/NumberRecognition.html),
by searching for a possible closed-form formula for a given number `y`,
in terms of arbitrary combinations of elementary and higher mathematical
functions.

The fundamental strategy is that, given a callable `f`, progressively more
complex rational arguments are tried, until a numerical match with the given
value `y` is found. By default, this match is searched up to linear
combinations with algebraic numbers (rationals or roots). Through the
`search_range` option the arguments tried can also be exact algebraic or
transcendental numbers, generated by the sibling packages
[algebraic-range](https://pypi.org/project/algebraic-range/) and
[transcendental-range](https://pypi.org/project/transcendental-range/).

When no functional form is specified, for each round of argument search,
a further search goes through the following common mathematical functions:
`sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `acot`, `log`, `exp`,
`sinh`, `cosh`, `tanh`, `asinh`, `acosh`, `atanh`, `acoth`,
`zeta`, `gamma`, `polygamma`, `erf`, `erfc`, `erfinv`,
`elliptic_k`, `elliptic_e`, `airyai`, `airybi`, `Ei`.
In addition, `find_closed_form` searches among algebraic combinations of the
following mathematical constants:
`pi`, `EulerGamma`, `Catalan`, `GoldenRatio`.

## Installation

```bash
pip install find-closed-form
```

To include the algebraic and transcendental search ranges:

```bash
pip install "find-closed-form[ranges]"
```

## Quick start

Find a possible mathematical function for a number:

```python
from find_closed_form import find_closed_form

find_closed_form(0.405465)          # log(3/2)
```

Find possible closed forms in terms of common mathematical functions:

```python
find_closed_form(3.792277)          # 1/6 + gamma(1/4)
```

Find formulae in terms of mathematical constants:

```python
find_closed_form(1.044866)          # 1/sqrt(Catalan)
```

Specify the functional form as a callable:

```python
from sympy import zeta

find_closed_form(1.85653, functions=lambda x: 1/zeta(x)**2)
# zeta(1/5)**(-2)
```

## Scope

The numerical match with the functional form is searched up to addition or
multiplication by an algebraic number (that is, a rational or root):

```python
from sympy import asinh, log, exp

find_closed_form(0.780653, functions=lambda x: asinh(x))
# sqrt(5)*asinh(4)/6

find_closed_form(7.443967, functions=lambda x: log(1 + exp(x)))
# 10*log(1 + exp(1/10))
```

Multi-argument functions are supported:

```python
from sympy import gamma, log

find_closed_form(6.263643,
    functions=lambda x, y: log(x)*log(y),
    search_range="Integer")
# 2*log(5)*log(7)

find_closed_form(14.911818,
    functions=lambda x, y: gamma(x)*gamma(y),
    search_range="Plain")
# gamma(1/6)*gamma(1/3)
```

Search through a list of functional forms:

```python
from sympy import sinh, cosh, sech, csch

find_closed_form(5.550045, functions=[
    lambda x: sinh(x), lambda x: cosh(x),
    lambda x: sech(x), lambda x: csch(x),
])
# 6*sech(2/5)
```

Multiple results can be requested through `max_results`:

```python
find_closed_form(0.405465, functions=lambda x: log(x), max_results=10)
# returns multiple results, first = log(3/2)
```

## Usage forms

The positional forms mirror the Wolfram Language resource function —
an integer second argument is the number of results, a callable (or list
of callables) the functional forms:

| Python | Wolfram Language |
|--------|------------------|
| `find_closed_form(y)` | `FindClosedForm[y]` |
| `find_closed_form(y, n)` | `FindClosedForm[y, n]` |
| `find_closed_form(y, f)` | `FindClosedForm[y, f]` |
| `find_closed_form(y, [f1, f2, ...])` | `FindClosedForm[y, {f1, f2, …}]` |
| `find_closed_form(y, f, n)` | `FindClosedForm[y, f, n]` |

The keyword equivalents `functions=f` and `max_results=n` are
interchangeable with the positional forms:

```python
find_closed_form(0.405465, lambda x: log(x), 10)   # same as above
find_closed_form(0.405465, 3)                      # FindClosedForm[y, n]:
# [log(3/2), -log(2/3), 7*airyai(1/2)/4]
```

## Options

### `algebraic_add`

Setting `algebraic_add=False` restricts the search to the specified functional
form up to multiplication (but not addition) by an algebraic number. This
can speed up the search, since special range properties are exploited for
certain known functions:

```python
from sympy import gamma

find_closed_form(0.1013578,
    functions=lambda x, y: 1/(gamma(x)*gamma(y)),
    algebraic_add=False)
# 1/(sqrt(pi)*gamma(1/6))
```

### `algebraic_factor`

Setting `algebraic_factor=False` restricts the search to the specified
functional form up to addition (but not multiplication) of an algebraic
number.

If both `algebraic_add` and `algebraic_factor` are set to `False`, the
search can be faster but may miss linear combinations of the functional form.

### `formula_complexity_threshold`

If not enough digits are specified, a careful balance between precision and
complexity of the result should be reached through `formula_complexity_threshold`.
Often the desired formula is the simplest. For example:

```python
from sympy import gamma

find_closed_form(38.94017, functions=lambda x: gamma(x),
    formula_complexity_threshold=15)
# 2*gamma(1/20)
```

The formula complexity is a positive real value which ranks complexity as
follows (matching the bug-fixed WL kernel 1.0.0.4): take all integers
appearing in the formula (expanding rationals, complex numbers and roots —
a root of degree *m/n* counts its base *|m|+|n|* times, and a non-positive
integer *j* counts as *−j+1*); for each integer, compute
`(5*digits + digit_sum + Ω + sqrt(i)) / 8`, where `Ω` is the number of
prime factors counted with multiplicity; then take the total.

### `max_search_rounds`

The maximum number of argument search rounds is 50 by default. This also
determines the largest integer argument and rational denominator reachable:

```python
from sympy import gamma

find_closed_form(49.44221, functions=lambda x: gamma(x),
    algebraic_add=False, algebraic_factor=False, search_range="Plain")
# gamma(1/50)
```

By default, larger arguments are not reachable:

```python
find_closed_form(59.43902, functions=lambda x: gamma(x),
    algebraic_add=False, algebraic_factor=False, search_range="Plain")
# None
```

Changing the value of `max_search_rounds` allows a solution to be found:

```python
find_closed_form(59.43902, functions=lambda x: gamma(x),
    max_search_rounds=100, algebraic_add=False, algebraic_factor=False,
    search_range="Plain")
# gamma(1/60)
```

### `rational_solutions`

By default, simple rational solutions are not returned, and more sophisticated
solutions are searched for. If `rational_solutions=True`, simple exact
rational solutions are allowed:

```python
from sympy import sin, pi

find_closed_form(0.25, functions=lambda x: sin(pi*x),
    rational_solutions=True, algebraic_add=False)
# 1/4
```

If the functional form is the identity, there is no need for this option:

```python
find_closed_form(0.25, functions=lambda x: x)
# 1/4
```

### `search_arguments`

Through `search_arguments` you can specify each particular argument
to be tried:

```python
from sympy import gamma
from fractions import Fraction

find_closed_form(4.678938, functions=lambda x: gamma(x),
    search_arguments=[Fraction(3), Fraction(1), Fraction(1, 3)])
# 2 + gamma(1/3)
```

This can speed up the search and serves as a debugging tool.

For multi-argument functions, a dict maps each slot to its own list
(the WL association `<|#1 -> list1, #2 -> list2|>`), with 1-based
integer or `"#1"` string keys:

```python
find_closed_form(1.32325, functions=lambda x, y: gamma(x)/gamma(y),
    search_arguments={1: [Fraction(1), Fraction(1, 2)],
                      2: [Fraction(3), Fraction(1), Fraction(1, 3)]})
# 2*sqrt(pi)/gamma(1/3)
```

Exact symbolic arguments are used as given, so an `algebraic_range` or
`transcendental_range` output can be passed directly:

```python
from sympy import exp
from algebraic_range import algebraic_range

find_closed_form(4.1132503787829275, functions=lambda x: exp(x),
    search_arguments=algebraic_range(0, 2, Fraction(1, 2)))
# exp(sqrt(2))
```

### `search_range`

By default, for each search round the arguments span the Farey range
`farey_range(-round, round, round)`, which consists of rationals of
uniform complexity. The following values are supported:

| Value | Range per round | Requires |
|-------|-----------------|----------|
| `"Farey"` | `farey_range(-cut, cut, cut)` — a rational Farey range | — |
| `"Plain"` | `range(-cut, cut, 1/cut)` — the shorter rational range | — |
| `"Integer"` | `range(-cut, cut)` — purely integer arguments | — |
| `"Algebraic"` | `algebraic_range(-cut, cut, 1/cut)` — exact roots | [algebraic-range](https://pypi.org/project/algebraic-range/) |
| `"Transcendental"` | `transcendental_range(-cut, cut, 1/cut)` — exact transcendental numbers | [transcendental-range](https://pypi.org/project/transcendental-range/) |

```python
from sympy import log

find_closed_form(6.263643,
    functions=lambda x, y: log(x)*log(y),
    search_range="Integer")
# 2*log(5)*log(7)
```

The `"Algebraic"` and `"Transcendental"` ranges search functions of
*exact algebraic or transcendental arguments*, going in the reverse
direction from a raw machine number to formulae such as `exp(sqrt(2))`
or `atan(log(2))`:

```python
find_closed_form(4.1132503787829275, search_range="Algebraic")
# exp(sqrt(2))

find_closed_form(0.606111934732855, search_range="Transcendental",
    search_range_options={"method": "log"})
# atan(log(2))
```

On these two ranges the default function list additionally includes the
identity, since the range elements are closed forms themselves — matched,
as always, up to algebraic factors and addends. Here the identity
recognizes the Gelfond–Schneider constant among the `'power'` elements
over algebraic generators:

```python
find_closed_form(2.665144142690225, search_range="Transcendental",
    search_range_options={"method": "power", "generators_domain": "algebraics"})
# 2**sqrt(2)
```

A callable is also accepted (a function of the search round, as in the
WL original), equivalent to `search_range_fn`:

```python
from fractions import Fraction
from algebraic_range import algebraic_range

find_closed_form(4.1132503787829275, functions=lambda x: exp(x),
    search_range=lambda cut: algebraic_range(-cut, cut, Fraction(1, cut)))
# exp(sqrt(2))
```

### `search_range_fn`

It is possible to specify a custom range function of the search round number:

```python
from sympy import log
from fractions import Fraction

find_closed_form(13.165149, functions=lambda x: log(x),
    search_range_fn=lambda cut: [Fraction(i) for i in range(0, 100*cut+1, 25)])
# sqrt(3)*log(2000)
```

### `search_range_options`

Extra keyword options for the `"Algebraic"` and `"Transcendental"` range
generators are forwarded through `search_range_options` — for example the
root orders of `algebraic_range`, or the transcendental function family
and multiplicity of `transcendental_range`:

```python
from sympy import exp

find_closed_form(3.5251431659552352, functions=lambda x: exp(x),
    search_range="Algebraic", search_range_options={"root_order": 3})
# exp(2**(1/3))
```

### `significant_digits`

The precision of the numerical match is automatically set to the number
of significant digits in the given number. If you want to ignore some
numerical error, you can specify a lower value:

```python
from sympy import zeta

find_closed_form(0.81248057539,
    functions=lambda x: 1/zeta(x)**2,
    significant_digits=7)
# zeta(11/3)**(-2)
```

### `search_time_limit`

The maximum time in seconds spent by the search algorithm. Default is 3600.
As in the WL original (`TimeConstrained`), the limit is enforced as a hard
interrupt — through `signal.setitimer`/`SIGALRM` on Unix main threads, with
cooperative clock checks elsewhere — and the results found before the
interrupt are still returned.

### Summary table

| Parameter | Default | Description |
|-----------|---------|-------------|
| `functions` | `None` | Functional forms to search; `None` uses ~31 common functions. |
| `max_results` | `1` | Number of results to return. |
| `significant_digits` | Auto | Precision target; auto-detected from input digits. |
| `formula_complexity_threshold` | Auto | Maximum formula complexity; auto-scaled per round. |
| `algebraic_factor` | `True` | Search up to multiplication by algebraic numbers. |
| `algebraic_add` | `True` | Search up to addition of algebraic numbers. |
| `rational_solutions` | `False` | Allow simple rational solutions. |
| `max_search_rounds` | `50` | Maximum argument-range expansion rounds. |
| `search_range` | `"Farey"` | `"Farey"`, `"Plain"`, `"Integer"`, `"Algebraic"`, `"Transcendental"`, or a callable. |
| `search_range_fn` | `None` | Custom `f(cut) → list` for argument generation. |
| `search_range_options` | `None` | Options forwarded to the `"Algebraic"`/`"Transcendental"` generators. |
| `search_arguments` | `None` | Fixed argument list or per-slot dict (bypasses auto ranges). |
| `search_time_limit` | `3600` | Maximum seconds for the search (hard `TimeConstrained`). |
| `monitor_search` | `False` | Print each result as it is found. |

## Properties and relations

`find_closed_form` with the identity function generalizes rationalization
and works with fewer digits:

```python
find_closed_form(0.666, functions=lambda x: x)   # 2/3
```

When the given number approximates a simple root, it also generalizes
root approximation:

```python
find_closed_form(4.243, functions=lambda x: x)    # 3*sqrt(2)
find_closed_form(0.5848, functions=lambda x: x)   # 5**(-1/3)
```

## Performance

On representative searches the port runs within ±2× of the Wolfram
Language 1.0.0 timings — substantially faster on the range-based
searches, slower on multi-argument functions (the WL `functionChamber`
optimization is not yet ported). Seven of ten benchmark cases return
symbolically identical results, the others equally precise alternative
matches. See
[benchmark/BENCHMARK.md](https://github.com/Daniele-Gregori/PyPI-packages/blob/main/packages/find-closed-form/benchmark/BENCHMARK.md)
for the case-by-case comparison and methodology.

## Auxiliary functions

The `formula_complexity` function is also exported and can be used directly
to compute the complexity of any sympy expression:

```python
from find_closed_form import formula_complexity
from sympy import Rational

formula_complexity(2*gamma(Rational(1, 20)))
```

The `farey_range` function generates Farey-based argument ranges:

```python
from find_closed_form import farey_range

farey_range(-3, 3, 3)
# [-3, -8/3, -5/2, ..., 5/2, 8/3, 3]
```

## Dependencies

- [sympy](https://www.sympy.org/) ≥ 1.12
- [farey](https://pypi.org/project/farey/) ≥ 0.7.0

Optional, for the `"Algebraic"` and `"Transcendental"` search ranges
(`pip install "find-closed-form[ranges]"`):

- [algebraic-range](https://pypi.org/project/algebraic-range/) ≥ 0.9.0
- [transcendental-range](https://pypi.org/project/transcendental-range/) ≥ 0.9.0 (Python ≥ 3.10)

## License

MIT
