Program Listing for File bz.hpp

Return to documentation for file (src/bz.hpp)

/* This file is part of brille.

Copyright © 2019,2020 Greg Tucker <greg.tucker@stfc.ac.uk>

brille is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.

brille is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with brille. If not, see <https://www.gnu.org/licenses/>.            */
#ifndef BRILLE_BZ_CLASS_H_
#define BRILLE_BZ_CLASS_H_
#include <omp.h>
#include "neighbours.hpp"
#include "transform.hpp"
#include "polyhedron.hpp"
#include "phonon.hpp"
#include "approx.hpp"
namespace brille {

class BrillouinZone {
  Reciprocal lattice;
  Reciprocal outerlattice;
  Polyhedron polyhedron;
  Polyhedron ir_polyhedron;
  bArray<double> ir_wedge_normals;
  bool time_reversal;
  bool has_inversion;
  bool is_primitive;
  bool no_ir_mirroring;
public:
  BrillouinZone(const Reciprocal& lat,
                const bool toprim=true,
                const int extent=1,
                const bool tr=false,
                const bool wedge_search=true
               ):
  lattice(toprim ? lat.primitive() : lat), outerlattice(lat), time_reversal(tr)
  {
    profile_update("Start of BrillouinZone construction");
    this->is_primitive = !(this->lattice.issame(this->outerlattice));
    this->has_inversion = this->time_reversal || lat.has_space_inversion();
    this->no_ir_mirroring = true;
    double old_volume = -1.0, new_volume=0.0;
    int test_extent = extent-1;
    // initial test_extent based on spacegroup or pointgroup?
    while (!brille::approx::scalar(new_volume, old_volume)){
      old_volume = new_volume;
      this->voro_search(++test_extent);
      new_volume = this->polyhedron.get_volume();
    }
    // fallback in case voro_search fails for some reason?!?
    if (brille::approx::scalar(new_volume, 0.)){
      throw std::runtime_error("voro_search failed to produce a non-null first Brillouin zone.");
    } else {
      verbose_update("New polyhedron volume ", this->polyhedron.get_volume());
      verbose_update("First Brillouin zone found using extent ",test_extent,", a ",this->polyhedron.string_repr());
    }
    // in case we've been asked to perform a wedge search for, e.g., P1 or P-1,
    // set the irreducible wedge now as the search will do nothing.
    this->ir_polyhedron = this->polyhedron;
    if (wedge_search){
      bool success = this->wedge_brute_force()
                  || this->wedge_brute_force(false,false) // no special 2-fold or mirror handling
                  || this->wedge_brute_force(false,true) // no special 2-fold handling (but special mirror handling)
                  || this->wedge_brute_force(true, false) // no special mirror handling (maybe not useful)
                  || this->wedge_brute_force(true, true, false) // last ditch effort, handle non order(2) operations in decreasing order
                  || this->wedge_brute_force(true, false, true, false)
                  ;
      // other combinations of special_2_folds, special_mirrors,
      // and sort_by_length are possible but not necessarily useful.
      if (!success)
        throw std::runtime_error("Failed to find an irreducible Brillouin zone.");
    }
    profile_update("  End of BrillouinZone construction");
  }
  void check_if_mirroring_needed(void){
    this->no_ir_mirroring = true;
    if (!this->has_inversion){
      PointSymmetry ps = this->outerlattice.get_pointgroup_symmetry(this->time_reversal?1:0);
      double goal = this->polyhedron.get_volume() / static_cast<double>(ps.size());
      double found = this->ir_polyhedron.get_volume();
      if (brille::approx::scalar(goal, 2.0*found)){
        /*The current Polyhedron at this->ir_polyhedron has half the anticipated
        volume. We can 'fix' this the easy way by mirroring the Polyhedron and
        gluing it onto the current version; the resultant polyhedron will come
        to a point at (0,0,0) and will not be convex. Instead, try to be clever
        and look for a convex combination of the found polyhedron and its
        mirror with one of the pointgroup operations applied:*/
        std::vector<Polyhedron> all_unions;
        Polyhedron mirrored = this->ir_polyhedron.mirror();
        for (auto & r: ps.getall())
          all_unions.push_back(this->ir_polyhedron + mirrored.rotate(r));
        // The combination of a polyhedron and its rotated inverse which has
        // the least number of vertices is (hopefully) convex.
        auto min_vert_union = std::min_element(
          all_unions.begin(),all_unions.end(),
          [](const Polyhedron & a, const Polyhedron & b){
            return a.num_vertices() < b.num_vertices();
          }
        );
        // Polyhedron::operator+() needs to be fixed. As of now it does not look
        // for extraneous faces (like internal faces which are coplanar and
        // pointing in opposite directions) or coplanar external faces which
        // share an edge. Until this is fixed cross our fingers and hope that we
        // created a convex polyhedron such that the convex hull of its points
        // gives the same polyhedron back:
        Polyhedron mvu_convex_hull(min_vert_union->get_vertices());
        if (brille::approx::scalar(goal, mvu_convex_hull.get_volume())){
          // we found a polyhedron with the right volume which is convex!
          // so we can keep this as *the* ir_polyhedron
          this->ir_polyhedron = mvu_convex_hull;
          // and we need to update the found volume for the check below
          found = goal;
        }
      }
      // if found == goal no mirroring is required.
      // if found == goal/2, mirroring is required.
      this->no_ir_mirroring = brille::approx::scalar(goal, found);
    }
  }

  bool check_ir_polyhedron(void);
  bool wedge_explicit(void);
  const Reciprocal get_lattice() const { return this->outerlattice;};
  const Reciprocal get_primitive_lattice() const { return this->lattice;};
  size_t vertices_count() const { return this->get_vertices().size(0);};
  size_t faces_count() const { return this->get_points().size(0);};
  // irreducible reciprocal space wedge
  void
  set_ir_wedge_normals(const LQVec<double>& x){
    bool already_same = this->outerlattice.issame(x.get_lattice());
    LQVec<double> xp(this->outerlattice);
    PrimitiveTransform PT(this->outerlattice.get_bravais_type());
    bool transform_needed = ( PT.does_anything() && this->lattice.issame(x.get_lattice()) );
    if (!(already_same || transform_needed))
      throw std::runtime_error("ir_wedge_normals must be in the standard or primitive lattice used to define the BrillouinZone object");
    if (transform_needed)  xp = transform_from_primitive(this->outerlattice,x);
    const LQVec<double> & xref = transform_needed ? xp : x;
    this->ir_wedge_normals = xref.get_hkl();
  }
  LQVec<double> get_ir_wedge_normals() const;
  LQVec<double> get_primitive_ir_wedge_normals() const;
  template<typename... A>
  void
  set_polyhedron(const LQVec<double>& v, const LQVec<double>& p, A... args){
    bool both_same = v.get_lattice().issame(p.get_lattice());
    if (!both_same)
      throw std::runtime_error("The vertices, and points of a polyhedron must all be in the same cooridinate system");
    bool is_outer = this->outerlattice.issame(v.get_lattice());
    bool is_inner = this->lattice.issame(v.get_lattice());
    LQVec<double> vp(this->outerlattice), pp(this->outerlattice);
    PrimitiveTransform PT(this->outerlattice.get_bravais_type());
    is_inner &= PT.does_anything();
    if (!(is_outer || is_inner))
      throw std::runtime_error("The polyhedron must be described in the conventional or primitive lattice used to define the BrillouinZone object");
    if (is_inner){
      vp = transform_from_primitive(this->outerlattice, v);
      pp = transform_from_primitive(this->outerlattice, p);
    }
    const LQVec<double> & vref = is_inner ? vp : v;
    const LQVec<double> & pref = is_inner ? pp : p;
    this->polyhedron = Polyhedron(vref.get_xyz(), pref.get_xyz(), args...);
  }
  template<typename... A>
  void
  set_ir_polyhedron(const LQVec<double>& v, const LQVec<double>& p, const LQVec<double>& n, A... args){
    bool all_same = v.get_lattice().issame(p.get_lattice()) && p.get_lattice().issame(n.get_lattice());
    if (!all_same)
      throw std::runtime_error("The vertices, points, and normals of a polyhedron must all be in the same cooridinate system");
    bool is_outer = this->outerlattice.issame(v.get_lattice());
    bool is_inner = this->lattice.issame(v.get_lattice());
    LQVec<double> vp(this->outerlattice), pp(this->outerlattice), np(this->outerlattice);
    PrimitiveTransform PT(this->outerlattice.get_bravais_type());
    is_inner &= PT.does_anything();
    if (!(is_outer || is_inner))
      throw std::runtime_error("The polyhedron must be described in the conventional or primitive lattice used to define the BrillouinZone object");
    if (is_inner){
      vp = transform_from_primitive(this->outerlattice, v);
      pp = transform_from_primitive(this->outerlattice, p);
      np = transform_from_primitive(this->outerlattice, n);
    }
    const LQVec<double> & vref = is_inner ? vp : v;
    const LQVec<double> & pref = is_inner ? pp : p;
    const LQVec<double> & nref = is_inner ? np : n;
    this->ir_polyhedron = Polyhedron(vref.get_xyz(), pref.get_xyz(), nref.get_xyz(), args...);
  }
  bool set_ir_vertices(const LQVec<double>& v){
    bool is_outer = this->outerlattice.issame(v.get_lattice());
    bool is_inner = this->lattice.issame(v.get_lattice());
    LQVec<double> vp(this->outerlattice);
    PrimitiveTransform PT(this->outerlattice.get_bravais_type());
    is_inner &= PT.does_anything();
    if (!(is_outer || is_inner))
      throw std::runtime_error("The polyhedron must be described in the conventional or primitive lattice used to define the BrillouinZone object");
    if (is_inner)
      vp = transform_from_primitive(this->outerlattice, v);
    const LQVec<double> & vref = is_inner ? vp : v;
    debug_update("Generate a convex polyhedron from\n", vref.to_string());
    this->ir_polyhedron = Polyhedron(vref.get_xyz());
    debug_update("Generated a ", this->ir_polyhedron.string_repr()," from the specified vertices");
    // check that the polyhedron we've specified has the desired properties
    if (this->check_ir_polyhedron()){
      // set the wedge normals as well
      this->set_ir_wedge_normals(this->get_ir_polyhedron_wedge_normals());
      return true;
    }
    return false;
  }
  Polyhedron get_polyhedron(void) const;
  LQVec<double> get_vertices(void) const;
  LQVec<double> get_points(void) const;
  LQVec<double> get_normals(void) const;
  LQVec<double> get_half_edges(void) const;
  LQVec<double> get_primitive_vertices(void) const;
  LQVec<double> get_primitive_points(void) const;
  LQVec<double> get_primitive_normals(void) const;
  std::vector<std::vector<int>> get_faces_per_vertex(void) const;
  std::vector<std::vector<int>> get_vertices_per_face(void) const;
  Polyhedron get_ir_polyhedron(const bool true_ir=true) const;
  LQVec<double> get_ir_vertices(void) const;
  LQVec<double> get_ir_points(void) const;
  LQVec<double> get_ir_normals(void) const;
  LQVec<double> get_ir_primitive_vertices(void) const;
  LQVec<double> get_ir_primitive_points(void) const;
  LQVec<double> get_ir_primitive_normals(void) const;
  std::vector<std::vector<int>> get_ir_faces_per_vertex(void) const;
  std::vector<std::vector<int>> get_ir_vertices_per_face(void) const;
  void print() const;
  void wedge_search(const bool prefer_basis_vectors=true, const bool parallel_ok=true);
  bool wedge_brute_force(bool special_2_folds = true, bool special_mirrors = true, bool sort_by_length=true, bool sort_one_sym=true);
  void wedge_triclinic(void);
  void irreducible_vertex_search();
  void voro_search(const int extent=1);
  bool isprimitive(void) const {return this->is_primitive;};

  template<class T>
  std::vector<bool>
  isinside(const LQVec<T>& p) const {
    bool isouter = this->outerlattice.issame(p.get_lattice());
    bool isinner = this->lattice.issame(p.get_lattice());
    if (!(isouter||isinner))
      throw std::runtime_error("Q points must be in the standard or primitive lattice");
    std::vector<bool> out(p.size(0), true);
    LQVec<double> points, normals;
    if (isouter){
      points = this->get_points();
      normals = this->get_normals();
    } else {
      points = this->get_primitive_points();
      normals = this->get_primitive_normals();
    }
    for (size_t i=0; i<p.size(0); ++i)
      out[i] = dot(normals, p.view(i)-points).all(brille::cmp::le, 0.);
    return out;
  }
  template<class T>
  std::vector<bool>
  isinside_wedge(const LQVec<T> &p, const bool constructing=false) const {
    bool isouter = this->outerlattice.issame(p.get_lattice());
    bool isinner = this->lattice.issame(p.get_lattice());
    if (!(isouter||isinner)){
      std::string msg = "Q points provided to BrillouinZone::isinside_wedge ";
      msg += "must be in the standard or primitive lattice ";
      msg += "used to define the BrillouinZone object";
      throw std::runtime_error(msg);
    }
    std::vector<bool> out(p.size(0), true);
    LQVec<double> normals;
    if (isouter)
      normals = this->get_ir_wedge_normals();
    else
      normals = this->get_primitive_ir_wedge_normals();
    if (normals.size(0)){ // with no normals *all* points are "inside" the wedge
      // If a pointgroup has inversion symmetry then for every point, p, there is
      // an equivalent point, -p. This indicates that a point p is already in
      // the irreducible wedge if it has n̂ᵢ⋅p ≥ 0 for all irredudible-bounding-
      // plane normals, ̂nᵢ, *or* the opposite -- n̂ᵢ⋅ p ≤ 0 for all ̂nᵢ.
      // Since we are interested in enforcing a smallest-possible irreducible
      // Brillouin zone, we want to exclude the all n̂ᵢ⋅ p ≤ 0 half of the
      // reciprocal wedge precisely because they are equivalent.
      // It is only in the case where a pointgroup *does not have* space-inversion
      // symmetry and time-reversal symmetry is to be excluded that we can allow
      // n̂ᵢ⋅ p ≤ 0 to be a valid in-wedge solution.
      // The Array all method has a special switched execution path
      // for checking whether all values are ≤ or ≥ a value simultaneously

      // when constructing the irreducible Brillouin zone we need to only consider
      // the ≥0 case so that we end up with a convex polyhedron. The ir_polyhedron
      // accessor method mirrors the half-polyhedron in this case, so when
      // identifying whether a point is inside of the irreducible Brillouin zone
      // we must allow for the ≤0 case as well.
      brille::cmp c = constructing||this->no_ir_mirroring ? brille::cmp::ge : brille::cmp::le_ge;
      for (size_t i=0; i<p.size(0); ++i)
        out[i] = dot(normals, p.view(i)).all(c, 0.);
    }
    return out;
  }
  bool
  moveinto(const LQVec<double>& Q, LQVec<double>& q, LQVec<int>& tau, const int threads=0) const
  {
    profile_update("BrillouinZone::moveinto called with ",threads," threads");
    omp_set_num_threads( (threads > 0) ? threads : omp_get_max_threads() );
    bool already_same = this->lattice.issame(Q.get_lattice());
    LQVec<double> Qprim(this->lattice);
    LQVec<double> qprim(this->lattice);
    LQVec<int> tauprim(this->lattice);
    PrimitiveTransform PT(this->outerlattice.get_bravais_type());
    bool transform_needed = ( PT.does_anything() && this->outerlattice.issame(Q.get_lattice()) );
    if (!(already_same || transform_needed)){
      std::string msg = "Q points provided to BrillouinZone::moveinto must be ";
      msg += "in the standard or primitive lattice used to define ";
      msg += "the BrillouinZone object";
      throw std::runtime_error(msg);
    }
    if (transform_needed)  Qprim = transform_to_primitive(this->outerlattice,Q);
    const LQVec<double> & Qsl = transform_needed ? Qprim : Q;
    LQVec<double> & qsl = transform_needed ? qprim : q;
    LQVec<int> & tausl = transform_needed? tauprim : tau;

    // the face centre points and normals in the primitive lattice:
    auto points = this->get_primitive_points();
    auto normals = this->get_primitive_normals();
    normals = normals/norm(normals); // ensure they're normalised
    auto taus = (2.0*points).round();
    auto taulen = norm(taus);
    size_t max_count = taus.size(0);
    // ensure that qsl and tausl can hold each qi and taui
    qsl.resize(Qsl.size(0));
    tausl.resize(Qsl.size(0));
    long long snQ = brille::utils::u2s<long long, size_t>(Qsl.size(0));
  #pragma omp parallel for default(none)\
  shared(Qsl, tausl, qsl, points, normals, taus, taulen, snQ, max_count)\
  schedule(dynamic)
    for (long long si=0; si<snQ; si++){
      size_t i = brille::utils::s2u<size_t, long long>(si);
      auto taui = Qsl.view(i).round();
      auto qi = Qsl.view(i) - taui;
      auto last_shift = taui;
      size_t count{0};
      while (count++ < max_count && dot(normals, qi-points).any(brille::cmp::gt,0.)){
        auto qi_dot_normals = dot(qi , normals);
        auto Nhkl = (qi_dot_normals/taulen).round().to_std();
        auto qidn = qi_dot_normals.to_std();
        if (std::any_of(Nhkl.begin(), Nhkl.end(), [](int a){return a > 0;})){
          int maxnm{0};
          size_t maxat{0};
          for (size_t j=0; j<Nhkl.size(); ++j)
                                                           // protect against oscillating by ±τ
          if (Nhkl[j]>0 && Nhkl[j]>=maxnm && (0==maxnm || (norm(taus.view(j)+last_shift).all(brille::cmp::gt, 0.) && qidn[j]>qidn[maxat]))){
            maxnm = Nhkl[maxat=j];
          }
          qi -= taus.view(maxat) * static_cast<double>(maxnm); // ensure we subtract LQVec<double>
          taui += taus.view(maxat) * maxnm; // but add LQVec<int>
          last_shift = taus.view(maxat) * maxnm;
        }
      }
      qsl.set(i, qi);
      tausl.set(i, taui);
    }
    if (transform_needed){ // then we need to transform back q and tau
      q   = transform_from_primitive(this->outerlattice,qsl);
      tau = transform_from_primitive(this->outerlattice,tausl);
    }
    auto allinside = this->isinside(q);
    if (std::count(allinside.begin(), allinside.end(), false) > 0){
      for (size_t i=0; i<Q.size(0); ++i) if (!allinside[i]){
        info_update("Q  =",Q.to_string(i)  ," tau  =",tau.to_string(i)  ," q  =",q.to_string(i));
        info_update("Qsl=",Qsl.to_string(i)," tausl=",tausl.to_string(i)," qsl=",qsl.to_string(i),"\n");
      }
      throw std::runtime_error("Not all points inside Brillouin zone");
      // return false;
    }
    return true; // otherwise an error has been thrown
  }
  bool
  ir_moveinto(
    const LQVec<double>& Q, LQVec<double>& q, LQVec<int>& tau,
    std::vector<size_t>& Ridx, std::vector<size_t>& invRidx, const int threads=0)
  const {
    profile_update("BrillouinZone::ir_moveinto called with ",threads," threads");
    omp_set_num_threads( (threads > 0) ? threads : omp_get_max_threads() );
    /* The Pointgroup symmetry information comes from, effectively, spglib which
    has all rotation matrices defined in the conventional unit cell -- which is
    our `outerlattice`. Consequently we must work in the outerlattice here.  */
    if (!this->outerlattice.issame(Q.get_lattice()))
      throw std::runtime_error("Q points provided to ir_moveinto must be in the standard lattice used to define the BrillouinZone object");
    // get the PointSymmetry object, containing all operations
    PointSymmetry psym = this->get_pointgroup_symmetry();
    // ensure q, tau, and Rm can hold one for each Q.
    size_t nQ = Q.size(0);
    auto Qshape = Q.shape();
    q.resize(Qshape);
    tau.resize(Qshape);
    Ridx.resize(nQ);
    invRidx.resize(nQ);
    // find q₁ₛₜ in the first Brillouin zone and τ ∈ [reciprocal lattice vectors]
    // such that Q = q₁ₛₜ + τ
    this->moveinto(Q, q, tau, threads);
    // by chance some first Bz points are likely already in the IR-Bz:
    std::vector<bool> in_ir = this->isinside_wedge(q);
    //LQVec<double> qj(Q.get_lattice(), 1u);
    auto lat = Q.get_lattice();
    // OpenMP 2 (VS) doesn't like unsigned loop counters
    size_t n_outside{0};
    long long snQ = brille::utils::u2s<long long, size_t>(nQ);
    #pragma omp parallel for default(none) shared(psym, Ridx, invRidx, q, in_ir, lat, snQ) reduction(+:n_outside) schedule(dynamic)
    for (long long si=0; si<snQ; ++si){
      size_t i = brille::utils::s2u<size_t, long long>(si);
      // any q already in the irreducible zone need no rotation → identity, but we need to find the index of E
      bool outside=!in_ir[i];
      if (outside){
        // for others find the jᵗʰ operation which moves qᵢ into the irreducible zone
        LQVec<double> qj(lat, 1u); // a place to hold the multiplication result
        for (size_t j=0; j<psym.size(); ++j) if (outside) {
          // The point symmetry matrices relate *real space* vectors! We must use
          // their transposes' to rotate reciprocal space vectors.
          brille::utils::multiply_matrix_vector(qj.ptr(0), transpose(psym.get(j)).data(), q.ptr(i));
          if ( this->isinside_wedge(qj)[0] ){ /* store the result */
            // and (Rⱼᵀ)⁻¹ ∈ G, such that Qᵢ = (Rⱼᵀ)⁻¹⋅qᵢᵣ + τᵢ.
            q.set(i, qj); // keep Rⱼᵀ⋅qᵢ as qᵢᵣ
            invRidx[i] = j; // Rⱼ *is* the inverse of what we want for ouput
            Ridx[i] = psym.get_inverse_index(j); // find the index of Rⱼ⁻¹
            outside = false;
          }
        }
      } else {
        invRidx[i] = Ridx[i] = psym.find_index({1,0,0, 0,1,0, 0,0,1});
      }
      if (outside) {
        ++n_outside;
        in_ir[i] = false;
      }
    }
    if (n_outside > 0) for (size_t i=0; i<nQ; ++i) if (!in_ir[i]){
      std::string msg = "Q = " + Q.to_string(i);
      msg += " is outside of the irreducible BrillouinZone ";
      msg += " : tau = " + tau.to_string(i) + " , q = " + q.to_string(i);
      throw std::runtime_error(msg);
      return false;
    }
    return true; // otherwise we hit the runtime error above
  }
  bool
  ir_moveinto_wedge(const LQVec<double>& Q, LQVec<double>& q, std::vector<std::array<int,9>>& R, const int threads=0) const {
    omp_set_num_threads( (threads > 0) ? threads : omp_get_max_threads() );
    /* The Pointgroup symmetry information comes from, effectively, spglib which
    has all rotation matrices defined in the conventional unit cell -- which is
    our `outerlattice`. Consequently we must work in the outerlattice here.  */
    if (!this->outerlattice.issame(Q.get_lattice()))
      throw std::runtime_error("Q points provided to ir_moveinto must be in the standard lattice used to define the BrillouinZone object");
    // get the PointSymmetry object, containing all operations
    PointSymmetry psym = this->outerlattice.get_pointgroup_symmetry(this->time_reversal);
    // ensure q and R can hold one for each Q.
    size_t nQ = Q.size(0);
    auto Qshape = Q.shape();
    q.resize(Qshape);
    R.resize(nQ);
    // by chance some first Bz points are likely already in the IR-wedge:
    std::vector<bool> in_ir = this->isinside_wedge(Q);
    //LQVec<double> qj(Q.get_lattice(), 1u);
    auto lat = Q.get_lattice();
    // OpenMP 2 (VS) doesn't like unsigned loop counters
    size_t n_outside{0};
    long long snQ = brille::utils::u2s<long long, size_t>(nQ);
    #pragma omp parallel for default(none) shared(psym, R, q, Q, in_ir, lat, snQ) reduction(+:n_outside) schedule(dynamic)
    for (long long si=0; si<snQ; ++si){
      size_t i = brille::utils::s2u<size_t, long long>(si);
      // any q already in the irreducible zone need no rotation → identity
      bool outside=!in_ir[i];
      if (outside){
        // for others find the jᵗʰ operation which moves qᵢ into the irreducible zone
        LQVec<double> qj(lat, 1u); // a place to hold the multiplication result
        for (size_t j=0; j<psym.size(); ++j) if (outside) {
          // The point symmetry matrices relate *real space* vectors! We must use
          // their transposes' to rotate reciprocal space vectors.
          brille::utils::multiply_matrix_vector(qj.ptr(0), transpose(psym.get(j)).data(), Q.ptr(i));
          if ( this->isinside_wedge(qj)[0] ){ /* store the result */
            q.set(i, qj); // keep Rⱼᵀ⋅Qᵢ as qᵢᵣ
            R[i] = transpose(psym.get_inverse(j)); // and (Rⱼᵀ)⁻¹ ∈ G, such that Q = (Rⱼᵀ)⁻¹⋅qᵢᵣ
            outside = false;
          }
        }
      } else {
        q.set(i, Q.view(i));
        R[i] = {1,0,0, 0,1,0, 0,0,1};
      }
      if (outside) {
        ++n_outside;
        in_ir[i] = false;
      }
    }
    if (n_outside > 0) for (size_t i=0; i<nQ; ++i) if (!in_ir[i]){
      std::string msg = "Q = " + Q.to_string(i);
      msg += " is outside of the irreducible reciprocal space wedge ";
      msg += " , irQ = " + q.to_string(i);
      throw std::runtime_error(msg);
      return false;
    }
    return true; // otherwise we hit the runtime error above
  }

  PointSymmetry get_pointgroup_symmetry() const{
    return this->outerlattice.get_pointgroup_symmetry(this->time_reversal);
  }
  int add_time_reversal() const {
    return this->time_reversal ? 1 : 0;
  }
private:
  template<class T, class R>
  bool
  wedge_normal_check(const LQVec<T>& n, LQVec<R>& normals, size_t& num){
    std::string msg = "Considering " + n.to_string(0) + "... ";
    if (norm(n).all(brille::cmp::eq, 0.0)){
      debug_update(msg, "rejected; zero-length");
      return false;
    }
    if (num==0){
      debug_update(msg, "accepted; first normal");
      normals.set(num, n.view(0));
      num=num+1;
      return true;
    }
    // num > 0 for all following views
    if (norm(cross(normals.view(0,num), n)).any(brille::cmp::eq, 0.)){
      debug_update(msg, "rejected; already present");
      return false;
    }
    normals.set(num,  n.view(0));
    if (this->ir_wedge_is_ok(normals.view(0,num+1))){
      debug_update(msg, "accepted");
      num=num+1;
      return true;
    }
    normals.set(num, -n.extract(0));
    if (this->ir_wedge_is_ok(normals.view(0,num+1))){
      debug_update(msg, "accepted (*-1)");
      num=num+1;
      return true;
    }
    debug_update(msg, "rejected; addition causes null wedge");
    return false;
  }
  template<class T, class R, class U>
  bool
  wedge_normal_check(const LQVec<T>& n0, const LQVec<R>& n1, LQVec<U>& normals, size_t& num){
    std::string msg = "Considering " + n0.to_string(0)+ " and " + n1.to_string(0) + "... ";
    bool p0=false, p1=false;
    if (num>0){
      p0 = norm(cross(normals.view(0,num), n0)).any(brille::cmp::eq,0.);
      p1 = norm(cross(normals.view(0,num), n1)).any(brille::cmp::eq,0.);
    }
    if (p0 && p1){
      debug_update(msg, "rejected; already present");
      return false;
    }
    if (num>0 && dot(normals.view(0,num)/norm(n0),n0/norm(n0)).any(brille::cmp::eq,1.)){
      normals.set(num,  n1.view(0));
      if (this->ir_wedge_is_ok(normals.view(0,num+1))){
        debug_update(msg, "n1 accepted (n0 present)");
        num=num+1;
        return true;
      }
      debug_update(msg, "n1 rejected (n0 present); addition causes null wedge");
      return false;
    }
    if (num>0 && dot(normals.view(0,num)/norm(n1),n1/norm(n1)).any(brille::cmp::eq,1.)){
      normals.set(num,  n0.view(0));
      if (this->ir_wedge_is_ok(normals.view(0,num+1))){
        debug_update(msg, "n0 accepted (n1 present)");
        num=num+1;
        return true;
      }
      debug_update(msg, "n0 rejected (n1 present); addition causes null wedge");
      return false;
    }
    if (num>0 && (p0 || p1)){
      debug_update_if(p0, msg, "-n0 present; addition causes null wedge)");
      debug_update_if(p1, msg, "-n1 present; addition causes null wedge)");
      return false;
    }
    normals.set(num,   n0.view(0));
    normals.set(num+1, n1.view(0));
    if (this->ir_wedge_is_ok(normals.view(0,num+2))){
      debug_update(msg, "n0 & n1 accepted");
      num=num+2;
      return true;
    }
    debug_update(msg, "n0 & n1 rejected; adding both would cause null wedge");
    return false;
  }
  void
  shrink_and_prune_outside(const size_t cnt, LQVec<double>& vrt, bArray<int>& ijk) const {
    verbose_update("shrinking to ",cnt);
    if(vrt.size(0) && ijk.size(0)){
      vrt.resize(cnt);
      ijk.resize(cnt);
      if (cnt){ // isinside has a problem with vrt.size()==0
        auto isin = this->isinside(vrt);
        verbose_update("and retaining ", std::count(isin.begin(), isin.end(), true), " inside vertices");
        vrt = vrt.extract(isin);
        ijk = ijk.extract(isin);
      }
    }
  }
  template<class T>
  bool
  ir_wedge_is_ok(const LQVec<T>& normals){
    this->set_ir_wedge_normals(normals); // assigns this->ir_wedge_normals
    this->irreducible_vertex_search(); // assigns this->ir_polyhedron
    return !brille::approx::scalar(this->ir_polyhedron.get_volume(), 0.0);
  }
  LQVec<double> get_ir_polyhedron_wedge_normals(void) const;
};

bool
intersect_at(const LQVec<double>& ni, const LQVec<double>& pi,
             const LQVec<double>& nj, const LQVec<double>& pj,
             const LQVec<double>& nk, const LQVec<double>& pk,
             LQVec<double>& intersect, const int idx);
bool
intersect_at(const LQVec<double>& ni, const LQVec<double>& pi,
             const LQVec<double>& nj, const LQVec<double>& pj,
             const LQVec<double>& nk,
             LQVec<double>& intersect, const int idx);
bool
intersect_at(const LQVec<double>& ni, const LQVec<double>& pi,
             const LQVec<double>& nj,
             const LQVec<double>& nk,
             LQVec<double>& intersect, const int idx);
bool
intersect_at(const LQVec<double>& ni,
             const LQVec<double>& nj,
             const LQVec<double>& nk,
             LQVec<double>& intersect, const int idx);
double
normals_matrix_determinant(const LQVec<double>& a, const LQVec<double>&b, const LQVec<double>& c);

} // end namespace brille
#endif