Here is a small package providing GroupConstants
function, that automatically replaces groups of constants with auto-generated single constants. It provides only partial answer to this question since it does not perform any algebraic simplifications, only structural replacements for groups of constants.
It works by parsing given expression and replacing sub-expressions, that don't contain (FreeQ
) symbols considered as variables, with auto generated parameters (C
). When a Flat
function is encountered, constant expressions, that are its arguments, are grouped together and replaced with single constant.
BeginPackage["ConstantsGrouping`"];
Unprotect["`*"];
ClearAll["`*"]
GroupConstants::usage = "\
GroupConstants[expr, vars] \
returns a List containing two elements. First element is given expr \
with subexpressions, not containing given vars, replaced by constants. \
Each constant replaces largest possible subexpression. \
Second element of returned list is an Association of constants, used in \
returned expression, to subexpressions they replaced.";
SecondPass::usage = "\
SecondPass \
is an option for GroupConstants that specifies whether used parser should \
perform second pass, on expression, to normalize constants enumeration.";
Begin["`Private`"];
ClearAll["`*"]
constantQ[expr_] := FreeQ[expr, $variablePattern]
(* Return unchanged expression excluded from parsing
(by default numeric expressions). *)
parse[excluded_ /; MatchQ[excluded, $excludedForm]] := excluded
(* Replace expression considered as constant with a generated parameter
and associate this parameter with said expression. *)
parse[const_?constantQ] :=
With[{generatedConst = $generatedParameters[++$constantsCounter, const]},
$constants[generatedConst] = const;
generatedConst
]
(* For Flat functions group all their constant arguments and replace
whole groups with single constants. *)
parse[(h_ /; MemberQ[Attributes[h], Flat])[args__]] :=
h @@ Replace[
If[MemberQ[Attributes[h], Orderless],
GatherBy
(* else *),
SplitBy
][{args}, constantQ]
,
{
const_?constantQ :> parse[h @@ const],
nonConst_ :> h @@ parse /@ nonConst
}
,
{1}
]
(* For non-Flat functions simply map parser to their arguments. *)
parse[h_[args__]] := parse /@ h[args]
(* Return any other expression unchanged. *)
parse[expr_] := expr
listToAlternatives[l_List] := Alternatives @@ l
listToAlternatives[expr_] := expr
Options[GroupConstants] = {
ExcludedForms -> {_?NumericQ},
GeneratedParameters -> (C[#] &),
SecondPass -> False
};
GroupConstants[expr_, vars_, OptionsPattern[]] :=
Block[
{
$variablePattern = listToAlternatives[vars],
$excludedForm = listToAlternatives@OptionValue[ExcludedForms],
$generatedParameters = OptionValue[GeneratedParameters],
$constantsCounter = 0,
$constants = <||>
},
If[TrueQ[OptionValue[SecondPass]],
With[{firstPass = parse[expr], $oldConstants = $constants},
$constantsCounter = 0;
{parse[firstPass], $constants /. $oldConstants}
]
(* else *),
{parse[expr], $constants}
]
]
End[];
Protect["`*"];
EndPackage[];
Two simple usage examples:
testExpr =
(2 a b x Log[y] z + 3 c + d + 4 Pi x^2 + E^(2 x))/(e y^(5 f g) + 5 h i x)
(* Replace constant groups with default parameters, treat x and y as variables.*)
GroupConstants[testExpr, {x, y}]
(* Replace constant groups with ki symbols, treat only x as a variable. *)
GroupConstants[testExpr, x, GeneratedParameters -> (Symbol["k" <> ToString[#]]&)]
Going back to examples from question.
We can compare "normalized" versions of two functions. To make sure that constants have the same enumeration we allow parser to make second pass over given expression. For functions from question we get:
{expr1, $} =
GroupConstants[(kCat e s)/(s + (k2 + kCat)/k1), s, SecondPass -> True]
{referenceExpr, $} = GroupConstants[(vMax s)/(kM + s), s, SecondPass -> True]
expr1 == referenceExpr
(* {(s C[2])/(s + C[1]), <|C[1] -> (k2 + kCat)/k1, C[2] -> e kCat|>} *)
(* {(s C[2])/(s + C[1]), <|C[1] -> kM, C[2] -> vMax|>} *)
(* True *)
Above works because, from point of view of position of variable s
and constants, those expressions have the same structure.
Comparison as above will fail if expressions have different structure (even if mathematically equivalent):
{expr2, $} =
GroupConstants[(k1 kCat e s)/(k1 s + k2 + kCat), s, SecondPass -> True]
expr2 == referenceExpr
(* {(s C[3])/(C[1] + s C[2]), <|C[1] -> k2 + kCat, C[2] -> k1, C[3] -> e k1 kCat|>} *)
(* (s C[3])/(C[1] + s C[2]) == (s C[2])/(s + C[1]) *)
Nonetheless I hope that GroupConstants
might be useful part of semi-automated simplifications.
FullSimply[]
applied to your function? $\endgroup$