Program Listing for File expr.cpp¶
↰ Return to documentation for file (SeQuant/core/utility/expr.cpp)
#include <SeQuant/core/container.hpp>
#include <SeQuant/core/expr.hpp>
#include <SeQuant/core/reserved.hpp>
#include <SeQuant/core/utility/expr.hpp>
#include <SeQuant/core/utility/expr_matcher.hpp>
#include <SeQuant/core/utility/indices.hpp>
#include <SeQuant/core/utility/macros.hpp>
#include <SeQuant/core/utility/string.hpp>
#include <range/v3/algorithm/all_of.hpp>
#include <range/v3/view/concat.hpp>
#include <algorithm>
#include <bitset>
#include <climits>
#include <optional>
#include <sstream>
#include <string>
namespace sequant {
// Top-level diff means a diff of the object instance itself without
// regard for any contained subexpressions
template <typename T>
std::string to_string(const Complex<T> &c) {
std::stringstream stream;
if (c.imag() == 0) {
stream << c.real();
} else if (c.real() == 0) {
stream << c.imag() << "*i";
} else if (c.imag() < 0) {
stream << "(" << c.real() << " - " << (-c.imag()) << "*i)";
} else {
stream << "(" << c.real() << " + " << c.imag() << "*i)";
}
return stream.str();
}
std::string toplevel_diff(const Constant &lhs, const Constant &rhs) {
if (lhs == rhs) {
return {};
}
return to_string(lhs.value()) + " vs. " + to_string(rhs.value());
}
std::string toplevel_diff(const Variable &lhs, const Variable &rhs) {
if (lhs == rhs) {
return {};
}
if (lhs.label() != rhs.label()) {
return toUtf8(lhs.label()) + " vs. " + toUtf8(rhs.label());
}
return (lhs.conjugated() ? "conjugated"
: "non-conjugated" + std::string(" vs. ")) +
(rhs.conjugated() ? "conjugated" : "non-conjugated");
}
std::string toplevel_diff(const Index &lhs, const Index &rhs);
template <typename LRange, typename RRange>
std::string diff_indices(const LRange &lhs, const RRange &rhs) {
auto lhs_size = std::distance(std::begin(lhs), std::end(lhs));
auto rhs_size = std::distance(std::begin(rhs), std::end(rhs));
if (lhs_size != rhs_size) {
return std::to_string(lhs_size) + " indices vs. " +
std::to_string(rhs_size) + " indices";
}
auto lhs_it = std::begin(lhs);
auto rhs_it = std::begin(rhs);
std::string diff;
for (std::size_t i = 0; i < static_cast<std::size_t>(lhs_size); ++i) {
const Index &lhs_idx = *lhs_it++;
const Index &rhs_idx = *rhs_it++;
std::string subdiff = toplevel_diff(lhs_idx, rhs_idx);
if (subdiff.empty()) {
continue;
}
if (!diff.empty()) {
diff += ", ";
}
diff += "#" + std::to_string(i + 1) + ": " + subdiff;
}
return diff;
}
std::string diff_spaces(const IndexSpace &lhs, const IndexSpace &rhs) {
if (lhs == rhs) {
return {};
}
const auto &lhs_attrs = lhs.attr();
const auto &rhs_attrs = rhs.attr();
std::stringstream stream;
using AttrSet = std::bitset<sizeof(std::uint32_t) * CHAR_BIT>;
if (lhs_attrs.type() != rhs_attrs.type()) {
stream << "Types differ: " << AttrSet(lhs.type().to_int32()) << " vs. "
<< AttrSet(rhs.type().to_int32());
} else if (lhs_attrs.qns() != rhs_attrs.qns()) {
stream << "QNs differ: " << AttrSet(lhs.qns().to_int32()) << " vs. "
<< AttrSet(rhs.qns().to_int32());
} else if (lhs.base_key() != rhs.base_key()) {
stream << "Base key differs: " << toUtf8(lhs.base_key()) << " vs. "
<< toUtf8(rhs.base_key());
} else if (lhs.approximate_size() != rhs.approximate_size()) {
stream << "Size differs: " << std::to_string(lhs.approximate_size())
<< " vs. " << std::to_string(rhs.approximate_size());
} else {
SEQUANT_UNREACHABLE;
}
SEQUANT_ASSERT(!stream.str().empty());
return stream.str();
}
std::string toplevel_diff(const Index &lhs, const Index &rhs) {
if (lhs == rhs) {
return {};
}
if (lhs.full_label() != rhs.full_label()) {
return toUtf8(lhs.full_label()) + " vs. " + toUtf8(rhs.full_label());
}
if (lhs.space() != rhs.space()) {
// No string representation of spaces, unfortunately
return "Spaces differ: " + diff_spaces(lhs.space(), rhs.space());
}
if (lhs.has_proto_indices() != rhs.has_proto_indices()) {
return (lhs.has_proto_indices() ? "with" : "without") +
std::string(" vs. ") +
(rhs.has_proto_indices() ? "with" : "without") + " proto-indices";
}
if (lhs.proto_indices() != rhs.proto_indices()) {
return "Proto indices differ: " +
diff_indices(lhs.proto_indices(), rhs.proto_indices());
}
if (lhs.tag() != rhs.tag()) {
return "Different tags";
}
// We have run out of ideas of what to check
SEQUANT_ABORT("Unexpected difference between indices");
}
std::string toplevel_diff(const Tensor &lhs, const Tensor &rhs) {
if (lhs == rhs) {
return {};
}
if (lhs.label() != rhs.label()) {
return "Names differ: " + toUtf8(lhs.label()) + " vs. " +
toUtf8(rhs.label());
}
if (lhs.slots().size() != rhs.slots().size()) {
return std::to_string(lhs.slots().size()) + " indices vs. " +
std::to_string(rhs.slots().size()) + " indices";
}
if (lhs.symmetry() != rhs.symmetry()) {
return "Symmetry differs: " + toUtf8(to_wstring(lhs.symmetry())) + " vs. " +
toUtf8(to_wstring(rhs.symmetry()));
}
if (lhs.column_symmetry() != rhs.column_symmetry()) {
return "Particle-Symmetry differs: " +
toUtf8(to_wstring(lhs.column_symmetry())) + " vs. " +
toUtf8(to_wstring(rhs.column_symmetry()));
}
if (lhs.braket_symmetry() != rhs.braket_symmetry()) {
return "BraKet-Symmetry differs: " +
toUtf8(to_wstring(lhs.braket_symmetry())) + " vs. " +
toUtf8(to_wstring(rhs.braket_symmetry()));
}
if (lhs.bra() != rhs.bra()) {
return "Bra indices differ: " + diff_indices(lhs.bra(), rhs.bra());
}
if (lhs.ket() != rhs.ket()) {
return "Ket indices differ: " + diff_indices(lhs.bra(), rhs.bra());
}
if (lhs.aux() != rhs.aux()) {
return "Aux indices differ: " + diff_indices(lhs.ket(), rhs.ket());
}
// Really, this shouldn't produce an empty diff as the objects compare as
// non-equal but we have run out of ideas of what to check
SEQUANT_ABORT("Unhandled difference between tensors");
}
std::string toplevel_diff(const Sum & /*lhs*/, const Sum & /*rhs*/) {
// There is no way two Sum objects can be different on the top-level
return {};
}
std::string toplevel_diff(const Product &lhs, const Product &rhs) {
if (lhs.scalar() != rhs.scalar()) {
return "Prefactor differs: " +
toplevel_diff(Constant(lhs.scalar()), Constant(rhs.scalar()));
}
return {};
}
std::string diff(const Expr &lhs, const Expr &rhs) {
if (lhs == rhs) {
return {};
}
if (lhs.type_id() != rhs.type_id()) {
return std::string("Types differ: ") + typeid(lhs).name() + " (" +
std::to_string(lhs.type_id()) + " vs. " + typeid(rhs).name() +
std::to_string(rhs.type_id());
}
auto lhs_begin = std::begin(lhs);
auto lhs_end = std::end(lhs);
auto rhs_begin = std::begin(rhs);
[[maybe_unused]] auto rhs_end = std::end(rhs);
auto lhs_size = std::distance(lhs_begin, lhs_end);
auto rhs_size = std::distance(lhs_begin, lhs_end);
if (lhs_size != rhs_size) {
return "Sizes differ: " + std::to_string(lhs_size) + " vs. " +
std::to_string(rhs_size);
}
std::string diff_str;
for (std::size_t i = 0; i < static_cast<std::size_t>(lhs_size); ++i) {
const Expr &lhs_nested = *(*lhs_begin++);
const Expr &rhs_nested = *(*rhs_begin++);
std::string nested_diff = diff(lhs_nested, rhs_nested);
if (nested_diff.empty()) {
continue;
}
if (diff_str.empty()) {
diff_str += "Subexpression diff begin:\n";
}
diff_str +=
"Sub-Expr #" + std::to_string(i + 1) + ":\n" + nested_diff + "\n";
}
if (!diff_str.empty()) {
diff_str += "Subexpression diff end";
return diff_str;
}
if (lhs.is<Sum>()) {
diff_str = toplevel_diff(lhs.as<Sum>(), rhs.as<Sum>());
} else if (lhs.is<Product>()) {
diff_str = toplevel_diff(lhs.as<Product>(), rhs.as<Product>());
} else if (lhs.is<Tensor>()) {
diff_str = toplevel_diff(lhs.as<Tensor>(), rhs.as<Tensor>());
} else if (lhs.is<Constant>()) {
diff_str = toplevel_diff(lhs.as<Constant>(), rhs.as<Constant>());
} else if (lhs.is<Variable>()) {
diff_str = toplevel_diff(lhs.as<Variable>(), rhs.as<Variable>());
} else {
SEQUANT_ABORT("Unhandled expression type");
}
return diff_str;
}
#define SEQUANT_EXPR_INVALID(message) \
if (msg) { \
*msg = message; \
} \
return false;
namespace {
container::map<Index, std::size_t> index_occurrence_counts(const Expr &expr) {
container::map<Index, std::size_t> counts;
auto visit = [&counts](const Expr &e, auto &self) -> void {
if (e.is<Tensor>()) {
for (const Index &ix : e.as<Tensor>().const_braketaux()) {
++counts[ix];
for (const Index &p : ix.proto_indices()) ++counts[p];
}
} else if (!e.is_atom()) {
for (const ExprPtr &sub : e) self(*sub, self);
}
};
visit(expr, visit);
return counts;
}
container::set<Index> proto_aware_externals(
const container::map<Index, std::size_t> &counts) {
container::set<Index> ext;
for (const auto &[ix, n] : counts)
if (n % 2 == 1) ext.insert(ix);
return ext;
}
} // namespace
bool is_valid(const ExprPtr &expr, std::string *msg) {
if (!expr) {
SEQUANT_EXPR_INVALID("Expression is null");
}
return is_valid(*expr, msg);
}
bool is_valid(const Expr &expr, std::string *msg) {
if (!expr.is_atom()) {
// Validate children first
for (const ExprPtr ¤t : expr) {
if (!is_valid(current, msg)) {
return false;
}
}
}
if (expr.is<Variable>()) {
// Nothing to validate
} else if (expr.is<Constant>()) {
const Constant &c = expr.as<Constant>();
if (denominator(c.value().real()) == 0) {
SEQUANT_EXPR_INVALID("Denominator of real part of constant is zero");
}
if (denominator(c.value().imag()) == 0) {
SEQUANT_EXPR_INVALID("Denominator of imaginary part of constant is zero");
}
} else if (expr.is<Tensor>()) {
// Nothing to validate
} else if (expr.is<Product>()) {
const Product &prod = expr.as<Product>();
auto factor = prod.scalar();
if (denominator(factor.real()) == 0) {
SEQUANT_EXPR_INVALID(
"Denominator of real part of product factor is zero");
}
if (denominator(factor.imag()) == 0) {
SEQUANT_EXPR_INVALID(
"Denominator of imaginary part of product factor is zero");
}
// Check that indices don't appear more than 2 times
container::map<Index, std::size_t> index_counter;
for (const ExprPtr &factor : prod.factors()) {
IndexGroups<> indices = get_unique_indices(*factor);
for (const Index &idx :
ranges::views::concat(indices.bra, indices.ket, indices.aux)) {
index_counter[idx] += 1;
}
}
for (const auto &[idx, count] : index_counter) {
if (count > 2) {
SEQUANT_EXPR_INVALID("Index " + toUtf8(idx.full_label()) +
" appears more than 2 times");
}
}
} else if (expr.is<Sum>()) {
// Verify that all summands have the same external indices
const Sum &sum = expr.as<Sum>();
// A proto-indexed (CSV) summand carries occ indices both as tensor slots
// and inside composite indices' proto-index lists; the slot-only
// get_unique_indices is then not invariant across summands and
// false-positives here. Detect this from the reference summand and, when
// present, validate with the proto-aware external-index set instead.
const auto ref_counts = index_occurrence_counts(*sum.summand(0));
const bool proto_indexed = std::ranges::any_of(
ref_counts,
[](const auto &kv) { return kv.first.has_proto_indices(); });
bool consistent;
if (proto_indexed) {
const container::set<Index> ref = proto_aware_externals(ref_counts);
consistent =
std::ranges::all_of(sum.summands(), [&ref](const ExprPtr &s) {
return proto_aware_externals(index_occurrence_counts(*s)) == ref;
});
} else {
auto extractor = [](const ExprPtr &expr) {
return get_unique_indices(expr);
};
const IndexGroups<> ref = extractor(sum.summand(0));
auto compare = [&ref](const IndexGroups<> &grps) {
const bool bra_ok = std::ranges::is_permutation(ref.bra, grps.bra);
const bool ket_ok = std::ranges::is_permutation(ref.ket, grps.ket);
const bool aux_ok = std::ranges::is_permutation(ref.aux, grps.aux);
if (bra_ok && ket_ok && aux_ok) {
return true;
}
if (aux_ok && !bra_ok && !ket_ok) {
// Bra and ket indices might have been swapped in case of braket
// symmetry Let's just allow for that here without explicitly checking
// the summand's symmetry
auto combined_ref = ranges::views::concat(ref.bra, ref.ket);
auto combined_cmp = ranges::views::concat(grps.bra, grps.ket);
return std::ranges::is_permutation(combined_ref, combined_cmp);
}
return false;
};
consistent = std::ranges::all_of(sum.summands(), compare, extractor);
}
if (!consistent) {
SEQUANT_EXPR_INVALID("Inconsistent external indices in sum");
}
} else if (expr.is<Power>()) {
// A Power (base^exponent) is valid iff its base is valid; the exponent is a
// rational and is always well-formed. Power is atomic (it exposes no
// subexpressions to the children loop above), so validate the base here.
if (!is_valid(expr.as<Power>().base(), msg)) return false;
} else {
SEQUANT_ASSERT(false, "Unsupported expression type in is_valid");
}
return true;
}
bool is_valid(const ResultExpr &expr, std::string *msg) {
if (!is_valid(expr.expression(), msg)) {
return false;
}
// We need to make sure to remove any symmetrizers from the expression in
// order to not mess up the determination of external indices
ExprPtr rhs = expr.expression().clone();
pop_tensor(rhs, reserved::antisymm_label());
pop_tensor(rhs, reserved::symm_label());
IndexGroups<> externals = get_unique_indices(rhs);
const bool bra_ok = std::ranges::is_permutation(expr.bra(), externals.bra);
const bool ket_ok = std::ranges::is_permutation(expr.ket(), externals.ket);
const bool braket_ok = [&]() -> bool {
if (!bra_ok && !ket_ok) {
// Allow for potential braket symmetry having changed position of
// bra and ket indices. For simplicity's sake, be lenient and don't
// try to explicitly check for expression symmetry
auto combined_expr = ranges::views::concat(expr.bra(), expr.ket());
auto combined_ext = ranges::views::concat(externals.bra, externals.ket);
return std::ranges::is_permutation(combined_expr, combined_ext);
}
return bra_ok && ket_ok;
}();
if (!braket_ok && !bra_ok) {
SEQUANT_EXPR_INVALID(
"Bra indices of result are inconsistent with the rhs expression");
}
if (!braket_ok && !ket_ok) {
SEQUANT_EXPR_INVALID(
"Ket indices of result are inconsistent with the rhs expression");
}
if (!std::ranges::is_permutation(expr.aux(), externals.aux)) {
SEQUANT_EXPR_INVALID(
"Aux indices of result are inconsistent with the rhs expression");
}
// TODO: check whether specified symmetries of result are fulfilled in the rhs
// expression
return true;
}
#undef SEQUANT_EXPR_INVALID
ExprPtr transform_expr(const ExprPtr &expr,
const container::map<Index, Index> &index_replacements,
Constant::scalar_type scaling_factor) {
return transform_expr(*expr, index_replacements, scaling_factor);
}
ExprPtr transform_expr(const Expr &expr,
const container::map<Index, Index> &index_replacements,
Constant::scalar_type scaling_factor) {
if (expr.is<Constant>() || expr.is<Variable>()) {
if (scaling_factor != 1) {
return ex<Constant>(scaling_factor) * expr.clone();
}
return expr.clone();
}
auto transform_tensor = [&index_replacements](const Expr &tensor) -> ExprPtr {
ExprPtr result = tensor.clone();
auto &result_tensor = result->as<AbstractTensor>();
transform_indices(result_tensor, index_replacements);
reset_tags(result_tensor);
return result;
};
auto transform_product = [&transform_tensor,
&scaling_factor](const Product &product) {
auto result = std::make_shared<Product>();
result->scale(product.scalar());
for (auto &&term : product) {
if (term->is<AbstractTensor>()) {
result->append(1, transform_tensor(*term));
} else if (term->is<Variable>() || term->is<Constant>()) {
result->append(1, term->clone());
} else {
throw Exception("Invalid Expr type in transform_product");
}
}
result->scale(scaling_factor);
return result;
};
if (expr.is<AbstractTensor>()) {
auto result = transform_tensor(expr);
if (scaling_factor != 1) {
result = result * ex<Constant>(scaling_factor);
}
return result;
} else if (expr.is<Product>()) {
auto result = transform_product(expr.as<Product>());
return result;
} else if (expr.is<Sum>()) {
auto result = std::make_shared<Sum>();
for (auto &term : expr) {
result->append(transform_expr(term, index_replacements, scaling_factor));
}
return result;
} else {
throw Exception("Invalid Expr type in transform_expr");
}
}
std::optional<ExprPtr> pop_tensor(ExprPtr &expression,
std::wstring_view label) {
std::optional<ExprPtr> tensor;
if (expression->is<Sum>()) {
Sum result{};
for (ExprPtr &term : expression.as<Sum>()) {
std::optional<ExprPtr> popped = pop_tensor(term, label);
if (!tensor.has_value()) {
tensor = popped;
}
SEQUANT_ASSERT(tensor == popped);
result.append(std::move(term));
}
expression.as<Sum>() = std::move(result);
return tensor;
}
if (expression->is<Product>()) {
Product result;
result.scale(expression.as<Product>().scalar());
for (ExprPtr &factor : expression.as<Product>().factors()) {
std::optional<ExprPtr> popped = pop_tensor(factor, label);
if (!tensor.has_value()) {
tensor = popped;
}
SEQUANT_ASSERT(!popped.has_value() || tensor == popped);
if (!factor.is<Constant>() || !factor.as<Constant>().is_zero()) {
result.append(1, std::move(factor), Product::Flatten::No);
}
}
if (result.size() > 1 || (result.size() == 1 && result.scalar() != 1)) {
expression.as<Product>() = std::move(result);
} else if (result.size() == 1) {
expression = std::move(result.factor(0));
} else {
expression = ex<Constant>(0);
}
return tensor;
}
if (expression->is<Tensor>()) {
if (expression.as<Tensor>().label() == label) {
tensor = expression;
expression = ex<Constant>(0);
}
return tensor;
}
if (expression->is_scalar()) {
return tensor;
}
throw Exception("Unhandled expression type in pop_tensor");
}
ExprPtr &replace(ExprPtr &expr, const ExprMatcher &target,
const Expr &replacement) {
if (!target.expr().is_atom()) {
throw Exception(
"Replacement of composite expressions is not yet implemented");
}
container::svector<std::size_t> index_mapping;
if (target.expr().is<AbstractTensor>()) {
// Figure out which indices are being reused between target and replacement
// (those are the ones we might need to perform replacements on)
auto target_slots = slots(target.expr().as<AbstractTensor>());
auto replacement_indices = get_used_indices(replacement);
for (const auto &[i, idx] : ranges::views::enumerate(target_slots)) {
if (!idx.nonnull()) {
continue;
}
if (std::ranges::find(replacement_indices, idx) !=
replacement_indices.end()) {
index_mapping.emplace_back(i);
}
}
}
if (*expr == target) {
expr = replacement.clone();
} else {
expr->visit(
[&](ExprPtr ¤t) {
if (*current == target) {
ExprPtr repl;
if (index_mapping.empty()) {
repl = replacement.clone();
} else {
// Ensure that all indices shared between target and replacement
// will also be shared with current and the actual replacement we
// want to use for it (this becomes relevant if cmp compares only
// equivalence instead of equality)
SEQUANT_ASSERT(current->is<AbstractTensor>());
SEQUANT_ASSERT(target.expr().is<AbstractTensor>());
const auto ¤t_tensor = current->as<AbstractTensor>();
const auto &target_tensor = target.expr().as<AbstractTensor>();
SEQUANT_ASSERT(num_slots(current_tensor) ==
num_slots(target_tensor));
auto current_slots = slots(current_tensor);
auto target_slots = slots(target_tensor);
container::map<Index, Index> replacements;
for (std::size_t i : index_mapping) {
if (target_slots[i] != current_slots[i]) {
replacements[target_slots[i]] = current_slots[i];
}
}
repl = transform_expr(replacement, replacements);
}
current = std::move(repl);
}
},
/*only_atoms*/ true);
}
return expr;
}
ResultExpr &replace(ResultExpr &expr, const ExprMatcher &target,
const Expr &replacement) {
replace(expr.expression(), target, replacement);
// We have to check whether the external indices have been modified by the
// replacement and if they did, adapt the indices in the result
IndexGroups<> externals = get_unique_indices(expr.expression());
if (!std::ranges::equal(externals.bra, expr.bra()) ||
!std::ranges::equal(externals.ket, expr.ket()) ||
!std::ranges::equal(externals.aux, expr.aux())) {
// Externals have changed -> update result
// TODO: Is retaining result symmetry a reasonable thing to do? Generally
// speaking, replacements could also change the result symmetry so in
// principle we'd need a way to deduce result symmetry.
expr =
ResultExpr(bra(std::move(externals.bra)), ket(std::move(externals.ket)),
aux(std::move(externals.aux)), expr.symmetry(),
expr.braket_symmetry(), expr.column_symmetry(),
expr.has_label() ? std::optional<std::wstring>(expr.label())
: std::nullopt,
std::move(expr.expression()));
}
return expr;
}
} // namespace sequant