Skip to content

Draft: Remove recursion limit workaround by updating to formulaic 0.5 #2

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doe/jacobian.py
Original file line number Diff line number Diff line change
@@ -72,7 +72,7 @@ def __init__(
self.vars = self.problem.inputs.names
self.n_vars = self.problem.n_inputs

self.model_terms = np.array(model.terms, dtype=str)
self.model_terms = np.array([str(t) for t in model], dtype=str)
self.n_model_terms = len(self.model_terms)

if jacobian_building_block is not None:
76 changes: 26 additions & 50 deletions doe/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import sys
import warnings
from copy import deepcopy
from itertools import combinations
@@ -184,13 +183,8 @@ def original_problem(self) -> opti.Problem:
def get_formula_from_string(
self,
model_type: Union[str, Formula] = "linear",
rhs_only: bool = True,
) -> Formula:
return get_formula_from_string(
model_type=model_type,
problem_context=self,
rhs_only=rhs_only,
)
return get_formula_from_string(model_type=model_type, problem_context=self)


def value2cat(value: pd.Series, input: opti.Categorical):
@@ -212,60 +206,44 @@ def value2discrete(value: np.float64, input: opti.Discrete):
def get_formula_from_string(
model_type: Union[str, Formula] = "linear",
problem_context: Optional[ProblemContext] = None,
rhs_only: bool = True,
) -> Formula:
"""Reformulates a string describing a model or certain keywords as Formula objects.
Args:
model_type (str or Formula): A formula containing all model terms.
problem_context (ProblemContext): A problem context that nests necessary information on
how to translate a problem to a formula. Contains a problem.
rhs_only (bool): The function returns only the right hand side of the formula if set to True.
Returns:
A Formula object describing the model that was given as string or keyword.
"""
# set maximum recursion depth to higher value
recursion_limit = sys.getrecursionlimit()
sys.setrecursionlimit(2000)
Returns:
A Formula object describing the model that was given as string or keyword.
"""
if isinstance(model_type, Formula):
return model_type
# build model if a keyword and a problem are given.
else:
# linear model
if model_type == "linear":
formula = linear_formula(problem_context=problem_context)

# linear and interactions model
elif model_type == "linear-and-quadratic":
formula = linear_and_quadratic_formula(problem_context=problem_context)

# linear and quadratic model
elif model_type == "linear-and-interactions":
formula = linear_and_interactions_formula(problem_context=problem_context)

# fully quadratic model
elif model_type == "fully-quadratic":
formula = fully_quadratic_formula(problem_context=problem_context)

else:
formula = model_type + " "
# linear model
if model_type == "linear":
formula = linear_formula(problem_context=problem_context)
# linear and interactions model
elif model_type == "linear-and-quadratic":
formula = linear_and_quadratic_formula(problem_context=problem_context)
# linear and quadratic model
elif model_type == "linear-and-interactions":
formula = linear_and_interactions_formula(problem_context=problem_context)
# fully quadratic model
elif model_type == "fully-quadratic":
formula = fully_quadratic_formula(problem_context=problem_context)
else:
formula = model_type + " "

formula = Formula(formula[:-3])

if rhs_only:
if hasattr(formula, "rhs"):
formula = formula.rhs

# set recursion limit to old value
sys.setrecursionlimit(recursion_limit)
if hasattr(formula, "rhs"):
return formula.rhs

return formula


def linear_formula(
problem_context: Optional[ProblemContext],
) -> str:
def linear_formula(problem_context: Optional[ProblemContext]) -> str:
"""Reformulates a string describing a linear-model or certain keywords as Formula objects.
formula = model_type + " "
@@ -394,18 +372,16 @@ def fully_quadratic_formula(
def n_zero_eigvals(
problem_context: ProblemContext, model_type: Union[str, Formula], epsilon=1e-7
) -> int:
"""Determine the number of eigenvalues of the information matrix that are necessarily zero because of
equality constraints."""
"""Determine the number of eigenvalues of the information matrix that are
necessarily zero because of equality constraints."""

# sample points (fulfilling the constraints)
model_formula = problem_context.get_formula_from_string(
model_type=model_type, rhs_only=True
)
N = len(model_formula.terms) + 3
formula = problem_context.get_formula_from_string(model_type)
N = sum(1 for _ in formula) + 3
X = problem_context.problem.sample_inputs(N)

# compute eigenvalues of information matrix
A = model_formula.get_model_matrix(X)
A = formula.get_model_matrix(X)
eigvals = np.abs(np.linalg.eigvalsh(A.T @ A))

return len(eigvals) - len(eigvals[eigvals > epsilon])
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -40,7 +40,7 @@ def get_version():
packages=["doe"],
python_requires=">=3.7",
install_requires=[
"formulaic==0.3.4",
"formulaic>=0.5",
"loguru",
"mopti",
"numpy",
401 changes: 147 additions & 254 deletions test/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import sys

import numpy as np
import opti
import pytest
@@ -20,259 +18,170 @@
)


def get_formula_from_string_recursion_limit():
# save recursion limit
recursion_limit = sys.getrecursionlimit()

# get formula for very large model
model = ""
for i in range(350):
model += f"x{i} + "
model = model[:-3]
model = get_formula_from_string(model_type=model)

terms = [f"x{i}" for i in range(350)]
terms.append("1")

for i in range(351):
assert model.terms[i] in terms
assert terms[i] in model.terms

assert recursion_limit == sys.getrecursionlimit()


def test_get_formula_from_string():
def test_formula_from_string():
problem = opti.Problem(
inputs=[opti.Continuous(f"x{i}", [0, 1]) for i in range(3)],
outputs=[opti.Continuous("y")],
)

problem_context = ProblemContext(problem)

# linear model
terms = ["1", "x0", "x1", "x2"]
model_formula = get_formula_from_string(
problem_context=problem_context, model_type="linear"
)
assert all(term in terms for term in model_formula.terms)
assert all(term in model_formula.terms for term in terms)
formula = get_formula_from_string("linear", problem_context)
terms = [str(t) for t in formula]
assert set(terms) == set(["1", "x0", "x1", "x2"])

# linear and interaction
terms = ["1", "x0", "x1", "x2", "x0:x1", "x0:x2", "x1:x2"]
model_formula = get_formula_from_string(
problem_context=problem_context, model_type="linear-and-interactions"
)
assert all(term in terms for term in model_formula.terms)
assert all(term in model_formula.terms for term in terms)
formula = get_formula_from_string("linear-and-interactions", problem_context)
terms = [str(t) for t in formula]
assert set(terms) == set(["1", "x0", "x1", "x2", "x0:x1", "x0:x2", "x1:x2"])

# linear and quadratic
terms = ["1", "x0", "x1", "x2", "x0**2", "x1**2", "x2**2"]
model_formula = get_formula_from_string(
problem_context=problem_context, model_type="linear-and-quadratic"
)
assert all(term in terms for term in model_formula.terms)
assert all(term in model_formula.terms for term in terms)
formula = get_formula_from_string("linear-and-quadratic", problem_context)
terms = [str(t) for t in formula]
assert set(terms) == set(["1", "x0", "x1", "x2", "x0**2", "x1**2", "x2**2"])

# fully quadratic
terms = [
"1",
"x0",
"x1",
"x2",
"x0:x1",
"x0:x2",
"x1:x2",
"x0**2",
"x1**2",
"x2**2",
]
model_formula = get_formula_from_string(
problem_context=problem_context, model_type="fully-quadratic"
formula = get_formula_from_string("fully-quadratic", problem_context)
terms = [str(t) for t in formula]
assert set(terms) == set(
[
"1",
"x0",
"x1",
"x2",
"x0:x1",
"x0:x2",
"x1:x2",
"x0**2",
"x1**2",
"x2**2",
]
)
assert all(term in terms for term in model_formula.terms)
assert all(term in model_formula.terms for term in terms)

# custom model
terms_lhs = ["y"]
terms_rhs = ["1", "x0", "x0**2", "x0:x1"]
model_formula = get_formula_from_string(
problem_context=problem_context,
model_type="y ~ 1 + x0 + x0:x1 + {x0**2}",
rhs_only=False,
)
assert all(term in terms_lhs for term in model_formula.terms.lhs)
assert all(term in model_formula.terms.lhs for term in terms_lhs)
assert all(term in terms_rhs for term in model_formula.terms.rhs)
assert all(term in model_formula.terms.rhs for term in terms_rhs)

# get formula without model: valid input
model = "x1 + x2 + x3"
model = get_formula_from_string(model_type=model)
assert str(model) == "1 + x1 + x2 + x3"

# get formula without model: invalid input
with pytest.raises(AssertionError):
model = get_formula_from_string("linear")
formula = get_formula_from_string(
problem_context=problem_context, model_type="y ~ 1 + x0 + x0:x1 + {x0**2}"
)
terms = [str(t) for t in formula]
assert set(terms) == set(["1", "x0", "x0**2", "x0:x1"])

# get formula for very large model
model = ""
for i in range(350):
model += f"x{i} + "
model = model[:-3]
model = get_formula_from_string(model_type=model)
# get formula without problem: valid input
formula = get_formula_from_string("x1 + x2 + x3")
terms = [str(t) for t in formula]
assert set(terms) == set(["1", "x1", "x2", "x3"])

terms = [f"x{i}" for i in range(350)]
terms.append("1")
# get formula without problem: invalid input
with pytest.raises(AssertionError):
model = get_formula_from_string("linear")

for i in range(351):
assert model.terms[i] in terms
assert terms[i] in model.terms
# get formula for very large model (formulaic < 0.5) runs into python's recursion limit
model = " + ".join(["1"] + [f"x{i}" for i in range(350)])
formula = get_formula_from_string(model_type=model)
assert set(model.split(" + ")) == set([str(t) for t in formula])


def test_formula_from_string_with_categoricals():
d = 2
inputs = [opti.Categorical(f"x{i+1}", ["a", "b", "c"]) for i in range(d)]
inputs.append(opti.Continuous(f"x{4}", [0, 1]))
problem = opti.Problem(
inputs=inputs,
inputs=[
opti.Categorical("x1", ["a", "b", "c"]),
opti.Categorical("x2", ["a", "b", "c"]),
opti.Continuous("x4", [0, 1]),
],
outputs=[opti.Continuous("y")],
)

# without relaxation
problem_context = ProblemContext(problem)

model_formula = problem_context.get_formula_from_string(model_type="linear")
# linear and interaction
terms = [
"1",
"x1",
"x2",
"x4",
]
model_formula = problem_context.get_formula_from_string(model_type="linear")
assert all(term in terms for term in model_formula.terms)
assert all(term in model_formula.terms for term in terms)

terms = [
"1",
"x1",
"x2",
"x4",
"x4**2",
]
model_formula = problem_context.get_formula_from_string(
model_type="linear-and-quadratic"
)
assert all(term in terms for term in model_formula.terms)
assert all(term in model_formula.terms for term in terms)

terms = [
"1",
"x1",
"x2",
"x4",
"x1:x4",
"x2:x4",
]
model_formula = problem_context.get_formula_from_string(
model_type="linear-and-interactions"
)
assert all(term in terms for term in model_formula.terms)
assert all(term in model_formula.terms for term in terms)

terms = [
"1",
"x1",
"x2",
"x4",
"x4**2",
"x1:x4",
"x2:x4",
]
model_formula = problem_context.get_formula_from_string(
model_type="fully-quadratic"
formula = problem_context.get_formula_from_string("linear")
assert set([str(t) for t in formula]) == set(["1", "x1", "x2", "x4"])

formula = problem_context.get_formula_from_string("linear-and-quadratic")
assert set([str(t) for t in formula]) == set(["1", "x1", "x2", "x4", "x4**2"])

formula = problem_context.get_formula_from_string("linear-and-interactions")
assert set([str(t) for t in formula]) == set(
["1", "x1", "x2", "x4", "x1:x4", "x2:x4"]
)

formula = problem_context.get_formula_from_string("fully-quadratic")
assert set([str(t) for t in formula]) == set(
["1", "x1", "x2", "x4", "x4**2", "x1:x4", "x2:x4"]
)
assert all(term in terms for term in model_formula.terms)
assert all(term in model_formula.terms for term in terms)

# with relaxation
problem_context = ProblemContext(problem)
problem_context.relax_problem()

model_formula = problem_context.get_formula_from_string(model_type="linear")
# linear and interaction
terms = [
"1",
"x1____a",
"x1____b",
"x1____c",
"x2____a",
"x2____b",
"x2____c",
"x4",
]
model_formula = problem_context.get_formula_from_string(model_type="linear")
assert all(term in terms for term in model_formula.terms)
assert all(term in model_formula.terms for term in terms)

terms = [
"1",
"x1____a",
"x1____b",
"x1____c",
"x2____a",
"x2____b",
"x2____c",
"x4",
"x4**2",
]
model_formula = problem_context.get_formula_from_string(
model_type="linear-and-quadratic"
)
assert all(term in terms for term in model_formula.terms)
assert all(term in model_formula.terms for term in terms)

terms = [
"1",
"x1____a",
"x1____b",
"x1____c",
"x2____a",
"x2____b",
"x2____c",
"x4",
"x1____a:x4",
"x1____b:x4",
"x1____c:x4",
"x2____a:x4",
"x2____b:x4",
"x2____c:x4",
]
model_formula = problem_context.get_formula_from_string(
model_type="linear-and-interactions"
)
assert all(term in terms for term in model_formula.terms)
assert all(term in model_formula.terms for term in terms)

terms = [
"1",
"x1____a",
"x1____b",
"x1____c",
"x2____a",
"x2____b",
"x2____c",
"x4",
"x1____a:x4",
"x1____b:x4",
"x1____c:x4",
"x2____a:x4",
"x2____b:x4",
"x2____c:x4",
"x4**2",
]
model_formula = problem_context.get_formula_from_string(
model_type="fully-quadratic"
formula = problem_context.get_formula_from_string("linear")
assert set([str(t) for t in formula]) == set(
[
"1",
"x1____a",
"x1____b",
"x1____c",
"x2____a",
"x2____b",
"x2____c",
"x4",
]
)

formula = problem_context.get_formula_from_string("linear-and-quadratic")
assert set([str(t) for t in formula]) == set(
[
"1",
"x1____a",
"x1____b",
"x1____c",
"x2____a",
"x2____b",
"x2____c",
"x4",
"x4**2",
]
)

formula = problem_context.get_formula_from_string("linear-and-interactions")
assert set([str(t) for t in formula]) == set(
[
"1",
"x1____a",
"x1____b",
"x1____c",
"x2____a",
"x2____b",
"x2____c",
"x4",
"x1____a:x4",
"x1____b:x4",
"x1____c:x4",
"x2____a:x4",
"x2____b:x4",
"x2____c:x4",
]
)

formula = problem_context.get_formula_from_string("fully-quadratic")
assert set([str(t) for t in formula]) == set(
[
"1",
"x1____a",
"x1____b",
"x1____c",
"x2____a",
"x2____b",
"x2____c",
"x4",
"x1____a:x4",
"x1____b:x4",
"x1____c:x4",
"x2____a:x4",
"x2____b:x4",
"x2____c:x4",
"x4**2",
]
)
assert all(term in terms for term in model_formula.terms)
assert all(term in model_formula.terms for term in terms)


def test_has_constraint_with_discrete_or_categorical():
@@ -362,25 +271,17 @@ def test_number_of_model_terms():

problem_context = ProblemContext(problem)

formula = get_formula_from_string(
problem_context=problem_context, model_type="linear"
)
assert len(formula.terms) == 6
formula = get_formula_from_string("linear", problem_context)
assert sum(1 for _ in formula) == 6

formula = get_formula_from_string(
problem_context=problem_context, model_type="linear-and-quadratic"
)
assert len(formula.terms) == 11
formula = get_formula_from_string("linear-and-quadratic", problem_context)
assert sum(1 for _ in formula) == 11

formula = get_formula_from_string(
problem_context=problem_context, model_type="linear-and-interactions"
)
assert len(formula.terms) == 16
formula = get_formula_from_string("linear-and-interactions", problem_context)
assert sum(1 for _ in formula) == 16

formula = get_formula_from_string(
problem_context=problem_context, model_type="fully-quadratic"
)
assert len(formula.terms) == 21
formula = get_formula_from_string("fully-quadratic", problem_context)
assert sum(1 for _ in formula) == 21

# 3 continuous & 2 discrete inputs
problem = opti.Problem(
@@ -396,25 +297,17 @@ def test_number_of_model_terms():

problem_context = ProblemContext(problem)

formula = get_formula_from_string(
problem_context=problem_context, model_type="linear"
)
assert len(formula.terms) == 6
formula = get_formula_from_string("linear", problem_context)
assert sum(1 for _ in formula) == 6

formula = get_formula_from_string(
problem_context=problem_context, model_type="linear-and-quadratic"
)
assert len(formula.terms) == 11
formula = get_formula_from_string("linear-and-quadratic", problem_context)
assert sum(1 for _ in formula) == 11

formula = get_formula_from_string(
problem_context=problem_context, model_type="linear-and-interactions"
)
assert len(formula.terms) == 16
formula = get_formula_from_string("linear-and-interactions", problem_context)
assert sum(1 for _ in formula) == 16

formula = get_formula_from_string(
problem_context=problem_context, model_type="fully-quadratic"
)
assert len(formula.terms) == 21
formula = get_formula_from_string("fully-quadratic", problem_context)
assert sum(1 for _ in formula) == 21


def test_constraints_as_scipy_constraints():