Engineering Units

Custom domain-specific engineering units can be defined such that they can be used in place of basic Constructive Solid Geometry (CSG) components (surfaces, cells, and universes) in a CSGBase instance. The primary purpose of an engineering unit is to serve as a shortcut for a collection of multiple basic CSG components, defined through a small set of intuitive, human-readable attributes rather than by defining each of the underlying components by hand.

To define a custom engineering unit, create a class that derives from one of the engineering unit class types: CSGSurfaceEngUnit for surface-like units, CSGCellEngUnit for cell-like units, and CSGUniverseEngUnit for universe-like units. Each of these classes derive from both the CSGEngUnit class and the corresponding base class (CSGSurface, CSGCell, and CSGUniverse, respectively). Therefore, any defined engineering unit can behave as the base component from which it derives, while benefiting from the functionality and convenience of being an engineering unit. A more detailed description of how to use engineering units in the generateCSG method can be found in CSGBase.

Common Methods

All custom engineering units must implement the following methods (first two methods are described in more detail below):

  • expandUnit(): creates the equivalent object using basic CSG components and adds them to the provided CSGBase instance

  • getAttributes(): returns a map of necessary unit attributes to their values

  • clone(): clones the instance as a unique pointer

All engineering units have additional information retrievable with the following methods defined in the abstract CSGEngUnit class:

  • getName(): returns the name of the unit

  • getBehavior(): returns what type of object this unit behaves like ("SURFACE", "CELL", or "UNIVERSE")

  • getUnitType(): returns the class name as a string for the specific engineering unit

Unit Expansion

All engineering units, regardless of type, must implement the expandUnit() method for the engineering unit. This method recreates the engineering unit as the corresponding rudimentary components (CSGSurface, CSGCell, CSGUniverse, and/or CSGLattice) and creates an object of the type of base component that it is being used as. The implementation of this method follows the same basic guidelines for implementing the generateCSG() method described in CSGBase. Every CSGEngUnit is initialized with and owns a scratch CSGBase object called _internal_base which is the base object that should be used for implementing the expansion method in expandUnit(). More details on implementing this method and the requirements for each of the three types of units is below in the respective sections.

commentnote:Naming Conventions and Restrictions

To avoid naming conflicts between units and other CSGBase components during the expansion, a good practice is to include the original unit's name (retrievable with getName()) as a part of the name for any generated component. Additionally, because the original unit is still present in the CSGBase instance until the full expansion process is complete, the final generated component cannot have the same name as the original unit. For example, if a CSGCellEngUnit is named my_cell, the expanded cell cannot also be named simply my_cell, but my_cell_expanded would be allowable.

Unit Attributes

The engineering unit attributes at a minimum are any pieces of data that are needed for a complete definition of the unit for any downstream connected codes or for defining the expansion method. For example, the minimum required information to include in the attributes for a regular N-sided polygon is the number of sides N and the apothem (center-to-flat distance). These attributes are what get returned in a map using the getAttributes() method, which gets called when producing the CSG JavaScript Object Notation (JSON) object in CSGBase.

commentnote:Attributes Data Type

The getAttributes() method returns a map of AttributeVariant data types (std::unordered_map<std::string, AttributeVariant>) to support flexibility in the type of data that might need to be defined. The AttributeVariant is a std::variant that can hold any of the following types: int, unsigned int, std::string, Real, bool, and vectors of these types (std::vector<int>, std::vector<unsigned int>, std::vector<std::string>, std::vector<Real>, std::vector<bool>).

CSGSurfaceEngUnit

To define a surface-like engineering unit, define a class that derives from CSGSurfaceEngUnit. Functionally, these behave like a CSGSurface in that you can use them to define half-spaces when defining CSGRegions for a CSGCell. All methods that are available on CSGSurface objects are also available on CSGSurfaceEngUnit objects except getCoeffs (not applicable to engineering units).

Expansion Implementation

The implementation of the expandUnit method must set the value of _expanded_region such that it defines the "negative" half-space CSGRegion of the original unit. The _expanded_region is returned by getExpandedRegion which is called during the expansion process via CSGBase. If expandUnit has not been called or _expanded_region was not set properly in the implementation, the getExpandedRegion query will return an error. To implement this, it is expected that additional CSGSurfaces are generated and added to the CSGBase object provided via _internal_base->addSurface(...).

Half-space Determination Implementation

All CSGSurfaceEngUnits are also derived from the CSGSurface and therefore must also implement the evaluateSurfaceEquationAtPoint method as described in CSGSurface. At a minimum, this should return a negative real number if a point is in the negative half-space of the unit and a positive value for the positive half-space. This value is used in getHalfspaceFromPoint in the same regard as standard CSGSurface objects.

CSGCellEngUnit

To define a cell-like engineering unit, define a class the derives from CSGCellEngUnit. Functionally, these behave like CSGCell objects in that you can add them to a CSGUniverse. Because CSGCellEngUnits are defined by custom human-readable attributes, the standard CSGCell attributes such as a cell fill or region are undefined and unable to be updated via the standard methods available to CSGCell.

Expansion Implementation

The implementation of the expandUnit method must set _expanded_cell to a pointer to a CSGCell object that defines the equivalent geometry. The _expanded_cell is returned by getExpandedCell which is called during the expansion process via CSGBase. If expandUnit has not been called or _expanded_cell was not set properly in the implementation, the getExpandedCell query will return an error. To implement this, it is expected that CSGSurfaces, and possibly CSGUniverses or other CSGCells, are generated and added to the CSGBase object provided.

The implementation of expandUnit should create exactly one CSGCell object that belongs to the root universe of _internal_base. This single cell will be taken as the expanded cell representation that replaces the CSGCellEngUnit in the parent CSGBase object. Additional nested universes, cells, and surfaces can be created in _internal_base to fully represent the engineering unit as necessary.

CSGUniverseEngUnit

To define a universe-like engineering unit, define a class the derives from CSGUniverseEngUnit. Functionally, these behave like a CSGUniverse object in that you can use them as fills for CSGCells or elements or outer for a CSGLattice. The expectation is that a CSGUniverseEngUnit replaces a collection of CSGCell objects using human-readable information, and therefore, the method addCell is not applicable to this unit type like it is a CSGUniverse.

Expansion Implementation

The implementation of expandUnit should populate the root universe of _internal_base with any cells necessary for the creation of the expanded CSGUniverse definition. The root universe of _internal_base will become the expanded universe used in the parent CSGBase object. After populating _internal_base with any necessary objects, the root universe will still be named ROOT_UNIVERSE by default. If this name is not updated and the original CSGBase object's root universe is also named ROOT_UNIVERSE, the expanded root universe will be renamed to [UnitName]_expanded_root to avoid potential name conflicts with root universe of the main CSGBase object.

Example Implementation

Below shows how an engineering unit to define an N-Sided Regular Polygon (CSGNPolygonUnit) as a CSGSurfaceEngUnit is implemented as an example.


#pragma once

#ifdef MOOSE_UNIT_TEST
#include "gtest/gtest.h"
#endif

#include "CSGSurfaceEngUnit.h"

namespace CSG
{

class CSGBase;

/**
 * CSGNPolygonUnit is a CSGSurfaceEngUnit that represents a regular N-sided polygon
 * (prismatic region) with its axis aligned with the z-axis.
 *
 * The polygon is defined by N infinite planes, one per side, each oriented so that
 * its inward normal points toward the center. The interior of the polygon is the
 * intersection of the N negative half-spaces of those planes.
 *
 * Implements:
 *   - expandUnit(): creates N CSGPlane surfaces in _internal_base, one per side
 *   - getExpandedRegion(): returns the intersection of the N half-spaces formed by the planes that
 *     containing the origin
 *   - evaluateSurfaceEquationAtPoint(): returns the maximum plane evaluation over
 *     all N sides; negative means the point is inside, positive means outside
 *   - clone(): returns a deep copy
 *   - getAttributes(): returns n_sides, apothem
 */
class CSGNPolygonUnit : public CSGSurfaceEngUnit
{
public:
  /**
   * @brief Construct a new CSGNPolygonUnit.
   *
   * @param name unique name of the unit
   * @param n_sides number of sides of the regular polygon (must be >= 3)
   * @param apothem distance from the center to the midpoint of each side
   */
  CSGNPolygonUnit(const std::string & name, int n_sides, Real apothem);

  /**
   * @brief Evaluate the polygon's surface equation at the given point.
   *
   * Returns the maximum signed plane-equation value over all N sides. A negative
   * return value means the point lies inside the polygon; positive means outside;
   * zero means on one of the sides.
   *
   * This can be evaluated before expansion using the stored geometric parameters.
   *
   * @param p point to evaluate
   * @return maximum signed distance from the point to any side (negative = inside)
   */
  Real evaluateSurfaceEquationAtPoint(const Point & p) const override;

  /**
   * @brief Return the polygon attributes for this object.
   *
   * @return map containing: num_sides (int), apothem (Real)
   */
  std::unordered_map<std::string, AttributeVariant> getAttributes() const override;

  // helper getter functions to get additional polygon dimensions/attributes

  /**
   * @brief Get the number of sides for the polygon
   *
   * @return number of sides
   */
  int getNumSides() { return _n_sides; }

  /**
   * @brief Get the polygon apothem (center-to-flat distance)
   *
   * @return apothem value
   */
  Real getApothem() { return _apothem; }

  /**
   * @brief Get the polygon side length
   *
   * @return side length
   */
  Real getSideLength() { return 2.0 * _apothem * std::tan(M_PI / _n_sides); }

  /**
   * @brief Get the the circumradius for the polygon (center-to-vertex distance)
   *
   * @return radius value
   */
  Real getRadius() { return _apothem / (std::cos(M_PI / _n_sides)); }

protected:
  /**
   * @brief Return a deep copy of this unit.
   *
   * @return unique_ptr to a new CSGNPolygonUnit with identical parameters
   */
  std::unique_ptr<CSGSurface> clone() const override
  {
    return std::make_unique<CSGNPolygonUnit>(_name, _n_sides, _apothem);
  }

  /**
   * @brief Create N CSGPlane surfaces in _internal_base, one per polygon side.
   *
   * Stores const pointers to the registered surfaces for use in getExpandedRegion().
   */
  void expandUnit() override;

private:
  /// Number of sides of the regular polygon
  const int _n_sides;

  /// Distance from the polygon center to the midpoint of each side
  const Real _apothem;

  /// Pointers to the expanded plane surfaces, populated during expandUnit()
  std::vector<const CSGSurface *> _planes;

#ifdef MOOSE_UNIT_TEST
  FRIEND_TEST(CSGEngUnitTest, testPolygonUnitExpansion);
  FRIEND_TEST(CSGEngUnitTest, testPolygonUnitClone);
#endif
};

} // namespace CSG
(framework/include/csg/CSGNPolygonUnit.h)

#include "CSGNPolygonUnit.h"
#include "CSGPlane.h"
#include "CSGBase.h" // complete type required to call methods on _internal_base

namespace CSG
{

CSGNPolygonUnit::CSGNPolygonUnit(const std::string & name, int n_sides, Real apothem)
  : CSGSurfaceEngUnit(name), _n_sides(n_sides), _apothem(apothem)
{
  if (_n_sides < 3)
    mooseError("N-sided polygon engineering unit " + name + " must have 3 or more sides.");
  if (_apothem <= 0.0)
    mooseError("N-sided polygon engineering unit " + name + " apothem must be positive.");
}

Real
CSGNPolygonUnit::evaluateSurfaceEquationAtPoint(const Point & p) const
{
  // Condition for interior (negative value) vs exterior (positive value) for the polygon is:
  // for point (x, y), if the following is true for all values of k, then the point is "interior".
  // If any one value is positive, then the point is outside the polygon. Therefore, we calculate
  // the maximum value. If that max value remains negative, then getHalfspaceFromPoint will
  // correctly determine the point to be interior.
  //
  //    x*cos(2*pi*k/N) + y*sin(2*pi*k/N) <= apothem, for all k = 0..N-1

  Real max_val =
      -std::numeric_limits<Real>::max(); // initialize to extremely large negative (to be updated)
  for (int k = 0; k < _n_sides; ++k)
  {
    auto val =
        (p(0) * std::cos(2.0 * M_PI * k / _n_sides) + p(1) * std::sin(2.0 * M_PI * k / _n_sides)) -
        _apothem;
    if (val > max_val)
      max_val = val;
  }
  return max_val;
}

std::unordered_map<std::string, AttributeVariant>
CSGNPolygonUnit::getAttributes() const
{
  return {{"num_sides", _n_sides}, {"apothem", _apothem}};
}

void
CSGNPolygonUnit::expandUnit()
{
  // Polygon orientation assumes an infinite prism oriented with the z-axis.
  // The right-most face is parallel to the y-axis and is centered at the origin.
  // Equation for the kth face, where the 0th face is the right-most and A is the apothem, follows
  // this equation:
  //    x*cos(2*pi*k/N) + y*sin(2*pi*k/N) + z*0 = A
  // Coefficients for the plane ax + by + cz = d:
  //    a = cos(2*pi*k/N)
  //    b = sin(2*pi*k/N)
  //    c = 0.0
  //    d = A (apothem)
  //
  // Surface naming scheme: [UnitName]_expanded_surf_[k]

  Real a, b; // to be calculated based on side
  Real c = 0.0;
  Real d = _apothem;

  // Initialize region to be added to:
  Point p(0, 0, 0); // origin used for determining half-space

  // base name for surfaces
  std::string base_name = getName() + "_expanded_surf_";

  for (int k = 0; k < _n_sides; ++k)
  {
    auto sname = base_name + std::to_string(k);
    a = std::cos(2.0 * M_PI * k / _n_sides);
    b = std::sin(2.0 * M_PI * k / _n_sides);
    std::unique_ptr<CSG::CSGPlane> s_ptr = std::make_unique<CSG::CSGPlane>(sname, a, b, c, d);
    auto & surf = _internal_base->addSurface(std::move(s_ptr));

    // determine the half-space that contains the origin for this surface
    auto hp_type = surf.getHalfspaceFromPoint(p);
    // half-space region for this surface only (to be intersected below)
    auto hp = (hp_type == CSGSurface::Halfspace::POSITIVE) ? +surf : -surf;

    // start the region with first half-space, otherwise intersect with existing region
    if (_expanded_region.getRegionType() == CSGRegion::RegionType::EMPTY)
      _expanded_region = hp;
    else
      _expanded_region &= hp; // intersect with existing region
  }
}

} // namespace CSG
(framework/src/csg/CSGNPolygonUnit.C)