diff --git a/cmake/FindCyclus.cmake b/cmake/FindCyclus.cmake new file mode 100644 index 0000000..f7e2e44 --- /dev/null +++ b/cmake/FindCyclus.cmake @@ -0,0 +1,207 @@ +# CYCLUS_CORE_FOUND - system has the Cyclus Core library +# CYCLUS_CORE_INCLUDE_DIR - the Cyclus include directory +# CYCLUS_CORE_LIBRARIES - The libraries needed to use the Cyclus Core Library +# CYCLUS_AGENT_TEST_LIBRARIES - A test library for agents +# CYCLUS_TEST_LIBRARIES - All testing libraries +# CYCLUS_DEFAULT_TEST_DRIVER - The default cyclus unit test driver + +# Check if we have an environment variable for cyclus root +if(DEFINED ENV{CYCLUS_ROOT_DIR}) + if(NOT DEFINED CYCLUS_ROOT_DIR) + set(CYCLUS_ROOT_DIR "$ENV{CYCLUS_ROOT_DIR}") + else() + message(STATUS "\tTwo CYCLUS_ROOT_DIRs have been found:") + message(STATUS "\t\tThe defined cmake variable CYCLUS_ROOT_DIR: ${CYCLUS_ROOT_DIR}") + message(STATUS "\t\tThe environment variable CYCLUS_ROOT_DIR: $ENV{CYCLUS_ROOT_DIR}") + endif() +elseif(DEFINED ENV{CONDA_PREFIX}) + if(NOT DEFINED CYCLUS_ROOT_DIR) + set(CYCLUS_ROOT_DIR "$ENV{CONDA_PREFIX}") + endif() +else() + find_program(CYCLUS_BIN cyclus) + if(CYCLUS_BIN) + execute_process( + COMMAND ${CYCLUS_BIN} --install-path + OUTPUT_VARIABLE CYCLUS_ROOT_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + else() + message(FATAL_ERROR + "Could not determine CYCLUS_ROOT_DIR. " + "Please set CYCLUS_ROOT_DIR or use a Conda environment with Cyclus installed.") + endif() +endif() + +# Let the user know if we're using a hint +message(STATUS "Using ${CYCLUS_ROOT_DIR} as CYCLUS_ROOT_DIR.") + +# Use $DEPS_ROOT_DIR if available +if(DEFINED DEPS_ROOT_DIR AND DEPS_ROOT_DIR) + set(DEPS_CYCLUS "${DEPS_ROOT_DIR}" + "${DEPS_ROOT_DIR}/cyclus") + set(DEPS_LIB_CYCLUS "${DEPS_ROOT_DIR}" + "${DEPS_ROOT_DIR}/cyclus" + "${DEPS_ROOT_DIR}/lib") + set(DEPS_SHARE_CYCLUS "${DEPS_ROOT_DIR}/share" + "${DEPS_ROOT_DIR}/share/cyclus") + set(DEPS_INCLUDE_CYCLUS "${DEPS_ROOT_DIR}/include" + "${DEPS_ROOT_DIR}/include/cyclus") +else() + set(DEPS_CYCLUS) + set(DEPS_LIB_CYCLUS) + set(DEPS_SHARE_CYCLUS) + set(DEPS_INCLUDE_CYCLUS) +endif() + +message(STATUS "-- Dependency Cyclus (DEPS_CYCLUS): ${DEPS_CYCLUS}") +message(STATUS "-- Dependency Library Cyclus (DEPS_LIB_CYCLUS): ${DEPS_LIB_CYCLUS}") +message(STATUS "-- Dependency Share Cyclus (DEPS_SHARE_CYCLUS): ${DEPS_SHARE_CYCLUS}") +message(STATUS "-- Dependency Include Cyclus (DEPS_INCLUDE_CYCLUS): ${DEPS_INCLUDE_CYCLUS}") + +# Set the include dir, this will be the future basis for other defined dirs +find_path(CYCLUS_CORE_INCLUDE_DIR cyclus.h + HINTS "${CYCLUS_ROOT_DIR}" "${CYCLUS_ROOT_DIR}/cyclus" + "${CYCLUS_ROOT_DIR}/include" + "${CYCLUS_ROOT_DIR}/include/cyclus" + ${DEPS_INCLUDE_CYCLUS} + /usr/local/cyclus /opt/local/cyclus + PATH_SUFFIXES cyclus/include include include/cyclus) + +# Set the include dir for test headers +find_path(CYCLUS_CORE_TEST_INCLUDE_DIR agent_tests.h + HINTS "${CYCLUS_ROOT_DIR}" "${CYCLUS_ROOT_DIR}/cyclus/tests" + "${CYCLUS_ROOT_DIR}/include" + "${CYCLUS_ROOT_DIR}/include/cyclus/tests" + ${DEPS_INCLUDE_CYCLUS} + /usr/local/cyclus /opt/local/cyclus + PATH_SUFFIXES cyclus/include include include/cyclus include/cyclus/tests cyclus/include/tests) + +# Add the root dir to the hints +set(CYCLUS_ROOT_DIR "${CYCLUS_CORE_INCLUDE_DIR}/../..") + +# Look for the shared data files +find_path(CYCLUS_CORE_SHARE_DIR cyclus.rng.in + HINTS "${CYCLUS_ROOT_DIR}" "${CYCLUS_ROOT_DIR}/cyclus" + "${CYCLUS_ROOT_DIR}/share" "${CYCLUS_ROOT_DIR}/share/cyclus" + ${DEPS_SHARE_CYCLUS} + /usr/local/cyclus /opt/local/cyclus + PATH_SUFFIXES cyclus/share share) + +# Look for the main library +find_library(CYCLUS_CORE_LIBRARY NAMES cyclus + HINTS "${CYCLUS_ROOT_DIR}" "${CYCLUS_ROOT_DIR}/cyclus" + ${DEPS_CYCLUS} + /usr/local/cyclus/lib /usr/local/cyclus + /opt/local /opt/local/cyclus + PATH_SUFFIXES cyclus/lib lib) + +# Optional libraries, only present if Cyclus was built with Cython support +find_library(CYCLUS_EVENTHOOKS_LIBRARY NAMES eventhooks + HINTS "${CYCLUS_ROOT_DIR}" "${CYCLUS_ROOT_DIR}/cyclus" + ${DEPS_CYCLUS} + /usr/local/cyclus/lib /usr/local/cyclus + /opt/local /opt/local/cyclus + PATH_SUFFIXES cyclus/lib lib) + +find_library(CYCLUS_PYINFILE_LIBRARY NAMES pyinfile + HINTS "${CYCLUS_ROOT_DIR}" "${CYCLUS_ROOT_DIR}/cyclus" + ${DEPS_CYCLUS} + /usr/local/cyclus/lib /usr/local/cyclus + /opt/local /opt/local/cyclus + PATH_SUFFIXES cyclus/lib lib) + +find_library(CYCLUS_PYMODULE_LIBRARY NAMES pymodule + HINTS "${CYCLUS_ROOT_DIR}" "${CYCLUS_ROOT_DIR}/cyclus" + ${DEPS_CYCLUS} + /usr/local/cyclus/lib /usr/local/cyclus + /opt/local /opt/local/cyclus + PATH_SUFFIXES cyclus/lib lib) + +# Look for the test libraries +find_library(CYCLUS_AGENT_TEST_LIBRARY NAMES baseagentunittests + HINTS "${CYCLUS_ROOT_DIR}" "${CYCLUS_ROOT_DIR}/cyclus" + ${DEPS_CYCLUS} + /usr/local/cyclus/lib /usr/local/cyclus + /opt/local /opt/local/cyclus + PATH_SUFFIXES cyclus/lib lib lib/cyclus) + +find_library(CYCLUS_GTEST_LIBRARY NAMES gtest + HINTS "${CYCLUS_ROOT_DIR}/lib/cyclus" + "${CYCLUS_ROOT_DIR}" "${CYCLUS_ROOT_DIR}/cyclus" + "${CYCLUS_ROOT_DIR}/lib" "${CYCLUS_CORE_SHARE_DIR}/../lib" + ${DEPS_LIB_CYCLUS} + /usr/local/cyclus/lib /usr/local/cyclus + /opt/local/lib /opt/local/cyclus/lib + PATH_SUFFIXES cyclus/lib lib) + +# Copy the results to the output variables. +if(CYCLUS_CORE_INCLUDE_DIR AND CYCLUS_CORE_TEST_INCLUDE_DIR + AND CYCLUS_CORE_LIBRARY AND CYCLUS_GTEST_LIBRARY + AND CYCLUS_CORE_SHARE_DIR AND CYCLUS_AGENT_TEST_LIBRARY) + + set(CYCLUS_CORE_FOUND 1) + set(CYCLUS_CORE_LIBRARIES "${CYCLUS_CORE_LIBRARY}") + + # If Cyclus was installed without Cython, these may not exist. + if(NOT "${CYCLUS_EVENTHOOKS_LIBRARY}" STREQUAL "CYCLUS_EVENTHOOKS_LIBRARY-NOTFOUND") + list(APPEND CYCLUS_CORE_LIBRARIES "${CYCLUS_EVENTHOOKS_LIBRARY}") + endif() + + if(NOT "${CYCLUS_PYINFILE_LIBRARY}" STREQUAL "CYCLUS_PYINFILE_LIBRARY-NOTFOUND") + list(APPEND CYCLUS_CORE_LIBRARIES "${CYCLUS_PYINFILE_LIBRARY}") + endif() + + if(NOT "${CYCLUS_PYMODULE_LIBRARY}" STREQUAL "CYCLUS_PYMODULE_LIBRARY-NOTFOUND") + list(APPEND CYCLUS_CORE_LIBRARIES "${CYCLUS_PYMODULE_LIBRARY}") + endif() + + set(CYCLUS_TEST_LIBRARIES "${CYCLUS_GTEST_LIBRARY}" "${CYCLUS_AGENT_TEST_LIBRARY}") + set(CYCLUS_AGENT_TEST_LIBRARIES "${CYCLUS_AGENT_TEST_LIBRARY}") + set(CYCLUS_CORE_INCLUDE_DIRS "${CYCLUS_CORE_INCLUDE_DIR}") + set(CYCLUS_CORE_TEST_INCLUDE_DIRS "${CYCLUS_CORE_TEST_INCLUDE_DIR}") + set(CYCLUS_CORE_SHARE_DIRS "${CYCLUS_CORE_SHARE_DIR}") + set(CYCLUS_DEFAULT_TEST_DRIVER "${CYCLUS_CORE_SHARE_DIR}/cyclus_default_unit_test_driver.cc") +else() + set(CYCLUS_CORE_FOUND 0) + set(CYCLUS_CORE_LIBRARIES) + set(CYCLUS_TEST_LIBRARIES) + set(CYCLUS_AGENT_TEST_LIBRARIES) + set(CYCLUS_CORE_INCLUDE_DIRS) + set(CYCLUS_CORE_TEST_INCLUDE_DIRS) + set(CYCLUS_CORE_SHARE_DIRS) + set(CYCLUS_DEFAULT_TEST_DRIVER) +endif() + +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CYCLUS_CORE_INCLUDE_DIR}/../../share/cyclus/cmake) + +# Report the results. +if(CYCLUS_CORE_FOUND) + set(CYCLUS_CORE_DIR_MESSAGE "Found Cyclus Core Headers : ${CYCLUS_CORE_INCLUDE_DIRS}") + set(CYCLUS_CORE_TEST_DIR_MESSAGE "Found Cyclus Core Test Headers : ${CYCLUS_CORE_TEST_INCLUDE_DIRS}") + set(CYCLUS_CORE_SHARE_MESSAGE "Found Cyclus Core Shared Data : ${CYCLUS_CORE_SHARE_DIRS}") + set(CYCLUS_CORE_LIB_MESSAGE "Found Cyclus Core Library : ${CYCLUS_CORE_LIBRARIES}") + set(CYCLUS_TEST_LIB_MESSAGE "Found Cyclus Test Libraries : ${CYCLUS_TEST_LIBRARIES}") + message(STATUS "${CYCLUS_CORE_DIR_MESSAGE}") + message(STATUS "${CYCLUS_CORE_TEST_DIR_MESSAGE}") + message(STATUS "${CYCLUS_CORE_SHARE_MESSAGE}") + message(STATUS "${CYCLUS_CORE_LIB_MESSAGE}") + message(STATUS "${CYCLUS_TEST_LIB_MESSAGE}") +else() + set(CYCLUS_CORE_DIR_MESSAGE + "Cyclus was not found. Make sure CYCLUS_CORE_LIBRARY and CYCLUS_CORE_INCLUDE_DIR are set.") + if(NOT Cyclus_FIND_QUIETLY) + message(STATUS "${CYCLUS_CORE_DIR_MESSAGE}") + message(STATUS "CYCLUS_CORE_SHARE_DIR was set to : ${CYCLUS_CORE_SHARE_DIR}") + message(STATUS "CYCLUS_CORE_LIBRARY was set to : ${CYCLUS_CORE_LIBRARY}") + message(STATUS "CYCLUS_TEST_LIBRARIES was set to : ${CYCLUS_GTEST_LIBRARY}") + if(Cyclus_FIND_REQUIRED) + message(FATAL_ERROR "${CYCLUS_CORE_DIR_MESSAGE}") + endif() + endif() +endif() + +mark_as_advanced( + CYCLUS_CORE_INCLUDE_DIR + CYCLUS_CORE_LIBRARY +) \ No newline at end of file diff --git a/cmake/cmake_uninstall.cmake.in b/cmake/cmake_uninstall.cmake.in new file mode 100644 index 0000000..78789af --- /dev/null +++ b/cmake/cmake_uninstall.cmake.in @@ -0,0 +1,22 @@ +if(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") + message(FATAL_ERROR "Cannot find install manifest: \"@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt\"") +endif(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") + +file(READ "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt" files) +string(REGEX REPLACE "\n" ";" files "${files}") +list(REVERSE files) +foreach(file ${files}) + message(STATUS "Uninstalling \"$ENV{DESTDIR}${file}\"") + if(EXISTS "$ENV{DESTDIR}${file}") + execute_process( + COMMAND @CMAKE_COMMAND@ -E remove "$ENV{DESTDIR}${file}" + OUTPUT_VARIABLE rm_out + RESULT_VARIABLE rm_retval + ) + if(NOT ${rm_retval} EQUAL 0) + message(FATAL_ERROR "Problem when removing \"$ENV{DESTDIR}${file}\"") + endif(NOT ${rm_retval} EQUAL 0) + else(EXISTS "$ENV{DESTDIR}${file}") + message(STATUS "File \"$ENV{DESTDIR}${file}\" does not exist.") + endif(EXISTS "$ENV{DESTDIR}${file}") +endforeach(file) \ No newline at end of file diff --git a/install.py b/install.py new file mode 100644 index 0000000..656c242 --- /dev/null +++ b/install.py @@ -0,0 +1,158 @@ +#! /usr/bin/env python3 +import os +import sys +import subprocess +import shutil + +try: + import argparse as ap +except ImportError: + import pyne._argparse as ap + + +def absexpanduser(x): + return os.path.abspath(os.path.expanduser(x)) + + +def default_prefix(): + return os.environ.get("CONDA_PREFIX", sys.prefix) + + +def check_windows_cmake(cmake_cmd): + if os.name != 'nt': + return cmake_cmd + + files_on_path = set() + for p in os.environ.get('PATH', '').split(';')[::-1]: + if os.path.exists(p): + try: + files_on_path.update(os.listdir(p)) + except OSError: + pass + + if 'cl.exe' in files_on_path: + pass + elif 'sh.exe' in files_on_path: + cmake_cmd += ['-G', 'MSYS Makefiles'] + elif 'gcc.exe' in files_on_path: + cmake_cmd += ['-G', 'MinGW Makefiles'] + + return cmake_cmd + + +def install(args): + if os.path.exists(args.build_dir) and args.clean_build: + shutil.rmtree(args.build_dir) + + if not os.path.exists(args.build_dir): + os.mkdir(args.build_dir) + + root_dir = os.path.abspath(os.path.dirname(__file__)) + + # Always re-run cmake unless build_only is intended to skip reconfigure. + cmake_cmd = ['cmake', root_dir] + + if args.prefix: + cmake_cmd += ['-DCMAKE_INSTALL_PREFIX=' + absexpanduser(args.prefix)] + + if args.cmake_prefix_path: + cmake_cmd += ['-DCMAKE_PREFIX_PATH=' + absexpanduser(args.cmake_prefix_path)] + + if args.coin_root: + cmake_cmd += ['-DCOIN_ROOT_DIR=' + absexpanduser(args.coin_root)] + + if args.cyclus_root: + cmake_cmd += ['-DCYCLUS_ROOT_DIR=' + absexpanduser(args.cyclus_root)] + + if args.boost_root: + cmake_cmd += ['-DBOOST_ROOT=' + absexpanduser(args.boost_root)] + + if args.build_type: + cmake_cmd += ['-DCMAKE_BUILD_TYPE=' + args.build_type] + + cmake_cmd = check_windows_cmake(cmake_cmd) + + subprocess.check_call(cmake_cmd, cwd=args.build_dir, + shell=(os.name == 'nt')) + + build_cmd = ['make'] + if args.threads: + build_cmd += ['-j' + str(args.threads)] + + subprocess.check_call(build_cmd, cwd=args.build_dir, + shell=(os.name == 'nt')) + + if args.test: + test_cmd = ['make', 'test'] + subprocess.check_call(test_cmd, cwd=args.build_dir, + shell=(os.name == 'nt')) + elif not args.build_only: + install_cmd = ['make', 'install'] + subprocess.check_call(install_cmd, cwd=args.build_dir, + shell=(os.name == 'nt')) + + +def uninstall(args): + makefile = os.path.join(args.build_dir, 'Makefile') + if not os.path.exists(args.build_dir) or not os.path.exists(makefile): + sys.exit("May not uninstall because it has not yet been built.") + + subprocess.check_call(['make', 'uninstall'], cwd=args.build_dir, + shell=(os.name == 'nt')) + + +def main(): + default_install = default_prefix() + + description = ( + "An installation helper script for building and installing this Cyclus " + "archetype module." + ) + parser = ap.ArgumentParser(description=description) + + parser.add_argument('--build_dir', default='build', + help='where to place the build directory') + + parser.add_argument('--uninstall', action='store_true', default=False, + help='uninstall') + + parser.add_argument('--clean-build', action='store_true', + help='remove the build directory before building') + + parser.add_argument('-j', '--threads', type=int, + help='number of threads to use in make') + + parser.add_argument('--prefix', default=default_install, + help='installation prefix (default: CONDA_PREFIX or sys.prefix)') + + parser.add_argument('--build-only', action='store_true', + help='only build the package, do not install') + + parser.add_argument('--test', action='store_true', + help='run tests after building') + + parser.add_argument('--coin_root', + help='path to the Coin-OR libraries directory') + + parser.add_argument('--cyclus_root', + help='path to Cyclus installation directory') + + parser.add_argument('--boost_root', + help='path to Boost libraries directory') + + parser.add_argument('--cmake_prefix_path', + help='CMAKE_PREFIX_PATH for find_package/find_library') + + parser.add_argument('--build_type', + help='CMAKE_BUILD_TYPE') + + args = parser.parse_args() + + if args.uninstall: + uninstall(args) + else: + install(args) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/us_inventory.cc b/src/us_inventory.cc index 9a348a3..a67411e 100644 --- a/src/us_inventory.cc +++ b/src/us_inventory.cc @@ -1,22 +1,50 @@ #include "us_inventory.h" +#include +#include #include #include #include -#include -#include -#include "toolkit/mat_query.h" // optional, but sometimes useful +namespace einstein { -USInventory::USInventory(cyclus::Context* ctx) : cyclus::Facility(ctx) {} +USInventory::USInventory(cyclus::Context* ctx) + : cyclus::Facility(ctx), + total_inventory_kg_(0.0) {} USInventory::~USInventory() {} +// ---------------------------------------------------------------------- +// Cyclus +// ---------------------------------------------------------------------- + +#pragma cyclus def schema einstein::USInventory +#pragma cyclus def annotations einstein::USInventory +#pragma cyclus def initinv einstein::USInventory +#pragma cyclus def snapshotinv einstein::USInventory +#pragma cyclus def infiletodb einstein::USInventory +#pragma cyclus def snapshot einstein::USInventory +#pragma cyclus def clone einstein::USInventory + +void USInventory::InitFrom(USInventory* m) { +#pragma cyclus impl initfromcopy einstein::USInventory +} + +void USInventory::InitFrom(cyclus::QueryableBackend* b) { +#pragma cyclus impl initfromdb einstein::USInventory +} + +// ---------------------------------------------------------------------- +// Basic facility methods +// ---------------------------------------------------------------------- + std::string USInventory::str() { std::stringstream ss; ss << cyclus::Facility::str() << " USInventory(outcommod=" << outcommod - << ", bins=" << bins_.size() << ")"; + << ", bins=" << bins_.size() + << ", total_inventory_kg=" << total_inventory_kg_ + << ")"; return ss.str(); } @@ -24,295 +52,460 @@ void USInventory::EnterNotify() { cyclus::Facility::EnterNotify(); if (outcommod.empty()) { - throw std::runtime_error("USInventory: outcommod is required."); + throw cyclus::ValueError("USInventory: outcommod is required."); } + if (assemblies_file.empty() || composition_file.empty()) { - throw std::runtime_error("USInventory: assemblies_file and composition_file are required."); + throw cyclus::ValueError( + "USInventory: assemblies_file and composition_file are required."); } bins_.clear(); idx_.clear(); + total_inventory_kg_ = 0.0; LoadAssembliesCSV_(assemblies_file); LoadCompositionCSV_(composition_file); - // Sanity check: every bin must have a composition - for (const auto& b : bins_) { - if (b.comp == nullptr) { - throw std::runtime_error("USInventory: missing composition for assembly_id=" + b.assembly_id); + // + for (size_t i = 0; i < bins_.size(); ++i) { + if (bins_[i].comp == NULL) { + throw cyclus::ValueError( + "USInventory: missing composition for assembly_id=" + + bins_[i].assembly_id); } } } -// ------------------------- DRE Methods ------------------------- +void USInventory::Tick() {} + +void USInventory::Tock() {} + +// ---------------------------------------------------------------------- +// DRE Methods +// ---------------------------------------------------------------------- void USInventory::GetMatlBids( cyclus::CommodMap::type& commod_requests, cyclus::BidPortfolio::type& bids) { - // Only respond to our commodity - auto it = commod_requests.find(outcommod); + cyclus::CommodMap::type::iterator it = + commod_requests.find(outcommod); + if (it == commod_requests.end()) { return; } - // Total available in all bins - double total_avail = 0.0; - for (const auto& b : bins_) total_avail += b.available_kg; + if (total_inventory_kg_ <= 0.0 || throughput_kg <= 0.0) { + return; + } - if (total_avail <= 0.0) return; + // Pick one valid composition for bid offers. + // This is just to create a valid offer object for the bid. + cyclus::Composition::Ptr bid_comp = NULL; + for (size_t i = 0; i < bins_.size(); ++i) { + if (bins_[i].available_kg > 0.0 && bins_[i].comp != NULL) { + bid_comp = bins_[i].comp; + break; + } + } - // Total requested mass - double total_req = 0.0; - for (auto& req : it->second) { - total_req += req->target()->quantity(); + if (bid_comp == NULL) { + return; } - if (total_req <= 0.0) return; - // Throughput limit per timestep - double can_supply = std::min(total_avail, throughput_kg); + double remaining_bid_capacity = std::min(total_inventory_kg_, throughput_kg); - // Create bids: simplest strategy = bid on every request with same “capacity” - // Cyclus will allocate trades; we finalize exact masses in GetMatlTrades. std::vector*>& reqs = it->second; + for (size_t i = 0; i < reqs.size(); ++i) { + cyclus::Request* req = reqs[i]; + double req_qty = req->target()->quantity(); + + if (req_qty <= 0.0 || remaining_bid_capacity <= 0.0) { + continue; + } + + double offer_qty = std::min(req_qty, remaining_bid_capacity); + cyclus::Material::Ptr offer = + cyclus::Material::CreateUntracked(offer_qty, bid_comp); + + bids.AddBid(req, offer, this, outcommod); + + // This prevents wildly overbidding across many requests. + remaining_bid_capacity -= offer_qty; + } +} + +size_t USInventory::ChooseBin_(double req_qty, bool full_only) const { + size_t best = bins_.size(); + + for (size_t i = 0; i < bins_.size(); ++i) { + const Bin& b = bins_[i]; + + if (b.comp == NULL || b.available_kg <= 0.0) { + continue; + } + + if (full_only && b.available_kg < req_qty) { + continue; + } - for (auto* r : reqs) { - // Bid some positive quantity. We'll honor final trade mass later. - // Use a dummy material for the bid with any valid comp (use first non-empty bin). - cyclus::Composition::Ptr c = nullptr; - for (const auto& b : bins_) { - if (b.available_kg > 0.0 && b.comp != nullptr) { c = b.comp; break; } + if (best == bins_.size()) { + best = i; + continue; } - if (c == nullptr) return; - double q = std::min(r->target()->quantity(), can_supply); - if (q <= 0.0) continue; + const Bin& cur = bins_[best]; - auto offer = cyclus::Material::Create(this->context(), q, c); - bids.AddBid(r, offer, this, outcommod); + if (selection_policy == "first") { + // keep the first eligible bin + continue; + } else if (selection_policy == "older") { + if (b.discharge_date < cur.discharge_date) { + best = i; + } + } else if (selection_policy == "newer") { + if (b.discharge_date > cur.discharge_date) { + best = i; + } + } else if (selection_policy == "highest_burnup") { + if (b.burnup > cur.burnup) { + best = i; + } + } else if (selection_policy == "lowest_burnup") { + if (b.burnup < cur.burnup) { + best = i; + } + } else if (selection_policy == "highest_enrichment") { + if (b.enrichment > cur.enrichment) { + best = i; + } + } else if (selection_policy == "lowest_enrichment") { + if (b.enrichment < cur.enrichment) { + best = i; + } + } else { + // fallback if user gives unknown policy + continue; + } } + + return best; } void USInventory::GetMatlTrades( - const std::vector< cyclus::Trade >& trades, - std::vector< std::pair< cyclus::Trade, - cyclus::Material::Ptr > >& responses) { + const std::vector >& trades, + std::vector, + cyclus::Material::Ptr> >& responses) { double remaining_throughput = throughput_kg; - for (const auto& tr : trades) { - double q = tr.amt; // requested trade quantity (kg) - if (q <= 0.0) continue; + for (size_t t = 0; t < trades.size(); ++t) { + const cyclus::Trade& tr = trades[t]; + double req_qty = tr.amt; + + if (req_qty <= 0.0) { + continue; + } - if (!allow_partial) { - // If partial not allowed, you can reject trades not fully satisfiable. - // Here we simply skip if we can't supply fully. - double total_avail = 0.0; - for (const auto& b : bins_) total_avail += b.available_kg; - if (q > total_avail || q > remaining_throughput) continue; + if (remaining_throughput <= 0.0 || total_inventory_kg_ <= 0.0) { + break; } - // Cap by throughput - double send = std::min(q, remaining_throughput); - if (send <= 0.0) break; - - // Withdraw mass from bins FIFO - // If you want “closest match” by burnup/enr later, this is where you’d choose bins differently. - cyclus::Composition::Ptr chosen_comp = nullptr; - double to_make = send; - - // We may need to split across bins with different compositions. - // For now: simplest policy = take from the first bin with enough mass. - // If not enough, we’ll take what we can from a bin and keep going, - // BUT composition would change. To keep it consistent per trade, - // we instead enforce: fulfill each trade from a single bin. - size_t chosen_i = bins_.size(); - for (size_t i = 0; i < bins_.size(); ++i) { - if (bins_[i].available_kg > 0.0) { - chosen_i = i; - break; + size_t chosen_i = ChooseBin_(req_qty, true); + double actual = 0.0; + + if (chosen_i != bins_.size()) { + actual = std::min(req_qty, remaining_throughput); + + if (!allow_partial && actual < req_qty) { + continue; + } + } else { + if (!allow_partial) { + continue; } + + chosen_i = ChooseBin_(req_qty, false); + if (chosen_i == bins_.size()) { + continue; + } + + actual = std::min(req_qty, + std::min(bins_[chosen_i].available_kg, + remaining_throughput)); + } + + if (chosen_i == bins_.size() || actual <= 0.0) { + continue; } - if (chosen_i == bins_.size()) continue; Bin& b = bins_[chosen_i]; - chosen_comp = b.comp; - double max_from_bin = std::min(b.available_kg, remaining_throughput); - double actual = allow_partial ? std::min(to_make, max_from_bin) : to_make; + actual = std::min(actual, b.available_kg); + actual = std::min(actual, remaining_throughput); - if (actual <= 0.0) continue; + if (actual <= 0.0) { + continue; + } - // Decrement inventory b.available_kg -= actual; + total_inventory_kg_ -= actual; remaining_throughput -= actual; - // Create the material response - auto mat = cyclus::Material::Create(this->context(), actual, chosen_comp); + cyclus::Material::Ptr mat = + cyclus::Material::Create(context(), actual, b.comp); + responses.push_back(std::make_pair(tr, mat)); } } -// ------------------------- CSV Loading ------------------------- +// ---------------------------------------------------------------------- +// CSV Helpers +// ---------------------------------------------------------------------- + +namespace { -static std::vector SplitCSVLine(const std::string& line) { - // Simple CSV splitter (no quoted commas). Good enough if your files are simple. +std::vector SplitCSVLine(const std::string& line) { std::vector out; std::stringstream ss(line); std::string item; + while (std::getline(ss, item, ',')) { - // trim whitespace - item.erase(item.begin(), std::find_if(item.begin(), item.end(), [](unsigned char ch){ return !std::isspace(ch); })); - item.erase(std::find_if(item.rbegin(), item.rend(), [](unsigned char ch){ return !std::isspace(ch); }).base(), item.end()); + item.erase(item.begin(), + std::find_if(item.begin(), item.end(), + [](unsigned char ch) { return !std::isspace(ch); })); + + item.erase(std::find_if(item.rbegin(), item.rend(), + [](unsigned char ch) { return !std::isspace(ch); }) + .base(), + item.end()); + out.push_back(item); } + return out; } +} // namespace + void USInventory::LoadAssembliesCSV_(const std::string& path) { std::ifstream f(path.c_str()); - if (!f) throw std::runtime_error("USInventory: cannot open " + path); + if (!f) { + throw cyclus::ValueError("USInventory: cannot open " + path); + } std::string header; - if (!std::getline(f, header)) throw std::runtime_error("USInventory: empty file " + path); - auto cols = SplitCSVLine(header); - - auto col_index = [&](const std::string& name) -> int { - for (size_t i = 0; i < cols.size(); ++i) if (cols[i] == name) return (int)i; - return -1; - }; + if (!std::getline(f, header)) { + throw cyclus::ValueError("USInventory: empty file " + path); + } - int i_id = col_index("assembly_id"); - int i_mass = col_index("total_mass_kg"); - int i_count = col_index("count"); // optional + std::vector cols = SplitCSVLine(header); + + int i_id = -1; + int i_mass = -1; + int i_count = -1; + int i_date = -1; + int i_bu = -1; + int i_enr = -1; + + for (size_t i = 0; i < cols.size(); ++i) { + if (cols[i] == "assembly_id") { + i_id = i; + } else if (cols[i] == "total_mass_kg") { + i_mass = i; + } else if (cols[i] == "count") { + i_count = i; + } else if (cols[i] == "discharge_date") { + i_date = i; + } else if (cols[i] == "burnup") { + i_bu = i; + } else if (cols[i] == "enrichment") { + i_enr = i; + } + } if (i_id < 0 || i_mass < 0) { - throw std::runtime_error("USInventory: assemblies.csv must contain assembly_id and total_mass_kg"); + throw cyclus::ValueError( + "USInventory: assemblies.csv must contain assembly_id and total_mass_kg"); } std::string line; while (std::getline(f, line)) { - if (line.empty()) continue; - auto v = SplitCSVLine(line); - if ((int)v.size() <= std::max(i_id, i_mass)) continue; + if (line.empty()) { + continue; + } + + std::vector v = SplitCSVLine(line); + if ((int)v.size() <= std::max(i_id, i_mass)) { + continue; + } Bin b; b.assembly_id = v[i_id]; double mass = std::stod(v[i_mass]); double count = 1.0; + if (i_count >= 0 && i_count < (int)v.size() && !v[i_count].empty()) { count = std::stod(v[i_count]); } + b.available_kg = mass * count; + if (i_date >= 0 && i_date < (int)v.size() && !v[i_date].empty()) { + b.discharge_date = std::stoi(v[i_date]); + } + + if (i_bu >= 0 && i_bu < (int)v.size() && !v[i_bu].empty()) { + b.burnup = std::stod(v[i_bu]); + } + + if (i_enr >= 0 && i_enr < (int)v.size() && !v[i_enr].empty()) { + b.enrichment = std::stod(v[i_enr]); + } + idx_[b.assembly_id] = bins_.size(); bins_.push_back(b); + total_inventory_kg_ += b.available_kg; } if (bins_.empty()) { - throw std::runtime_error("USInventory: no rows loaded from " + path); + throw cyclus::ValueError("USInventory: no rows loaded from " + path); } } void USInventory::LoadCompositionCSV_(const std::string& path) { std::ifstream f(path.c_str()); - if (!f) throw std::runtime_error("USInventory: cannot open " + path); + if (!f) { + throw cyclus::ValueError("USInventory: cannot open " + path); + } std::string header; - if (!std::getline(f, header)) throw std::runtime_error("USInventory: empty file " + path); - auto cols = SplitCSVLine(header); + if (!std::getline(f, header)) { + throw cyclus::ValueError("USInventory: empty file " + path); + } + + std::vector cols = SplitCSVLine(header); - auto col_index = [&](const std::string& name) -> int { - for (size_t i = 0; i < cols.size(); ++i) if (cols[i] == name) return (int)i; - return -1; - }; + int i_id = -1; + int i_nuc = -1; + int i_frac = -1; - int i_id = col_index("assembly_id"); - int i_nuc = col_index("nuclide"); - int i_frac = col_index("mass_fraction"); + for (size_t i = 0; i < cols.size(); ++i) { + if (cols[i] == "assembly_id") { + i_id = i; + } else if (cols[i] == "nuclide") { + i_nuc = i; + } else if (cols[i] == "mass_fraction") { + i_frac = i; + } + } if (i_id < 0 || i_nuc < 0 || i_frac < 0) { - throw std::runtime_error("USInventory: composition.csv must contain assembly_id, nuclide, mass_fraction"); + throw cyclus::ValueError( + "USInventory: composition.csv must contain assembly_id, nuclide, mass_fraction"); } - // Build comp maps per assembly_id std::unordered_map compmaps; std::string line; while (std::getline(f, line)) { - if (line.empty()) continue; - auto v = SplitCSVLine(line); - if ((int)v.size() <= std::max({i_id, i_nuc, i_frac})) continue; + if (line.empty()) { + continue; + } + + std::vector v = SplitCSVLine(line); + if ((int)v.size() <= std::max(i_id, std::max(i_nuc, i_frac))) { + continue; + } std::string aid = v[i_id]; std::string nuc = v[i_nuc]; double frac = std::stod(v[i_frac]); - if (frac <= 0.0) continue; + + if (frac <= 0.0) { + continue; + } int nid = NucIdFromString_(nuc); - compmaps[aid][nid] += frac; // += in case of duplicates + compmaps[aid][nid] += frac; } - // Create cyclus compositions and assign to bins - for (auto& kv : compmaps) { - const std::string& aid = kv.first; - auto it = idx_.find(aid); - if (it == idx_.end()) continue; // composition for an assembly not in assemblies.csv (skip) - // CreateFromMass expects mass fractions or masses; relative values are fine. - bins_[it->second].comp = cyclus::Composition::CreateFromMass(kv.second); + for (std::unordered_map::iterator it = + compmaps.begin(); + it != compmaps.end(); ++it) { + std::unordered_map::iterator idx_it = + idx_.find(it->first); + + if (idx_it == idx_.end()) { + continue; + } + + bins_[idx_it->second].comp = + cyclus::Composition::CreateFromMass(it->second); } } -// ------------------------- Nuclide Parsing ------------------------- +// ---------------------------------------------------------------------- +// Nuclide Parsing +// should use material class instead +// ---------------------------------------------------------------------- int USInventory::NucIdFromString_(const std::string& s) const { - // Minimal parser for strings like "U235", "Pu239", "Cs137". - // Produces zzaaam * 10? Cyclus commonly uses zzaaam format (e.g., 922350). - // We'll return zzaaam where m=0. - // If your dataset uses other forms (e.g., "U-235" or "92235"), tell me and I’ll adjust. - std::string t = s; - // Remove '-' and spaces - t.erase(std::remove_if(t.begin(), t.end(), [](unsigned char c){ return c=='-' || std::isspace(c); }), t.end()); - if (t.empty()) throw std::runtime_error("Bad nuclide string: '" + s + "'"); - // Split leading letters (element) + trailing digits (A) + t.erase(std::remove_if(t.begin(), t.end(), + [](unsigned char c) { + return c == '-' || std::isspace(c); + }), + t.end()); + + if (t.empty()) { + throw cyclus::ValueError("Bad nuclide string: '" + s + "'"); + } + size_t pos = 0; - while (pos < t.size() && std::isalpha((unsigned char)t[pos])) pos++; - if (pos == 0 || pos == t.size()) throw std::runtime_error("Bad nuclide string: '" + s + "'"); + while (pos < t.size() && std::isalpha(static_cast(t[pos]))) { + ++pos; + } + + if (pos == 0 || pos == t.size()) { + throw cyclus::ValueError("Bad nuclide string: '" + s + "'"); + } std::string sym = t.substr(0, pos); std::string a_str = t.substr(pos); - // Normalize symbol: first uppercase, rest lowercase - sym[0] = std::toupper((unsigned char)sym[0]); - for (size_t i = 1; i < sym.size(); ++i) sym[i] = std::tolower((unsigned char)sym[i]); + sym[0] = std::toupper(static_cast(sym[0])); + for (size_t i = 1; i < sym.size(); ++i) { + sym[i] = std::tolower(static_cast(sym[i])); + } int A = std::stoi(a_str); - if (A <= 0) throw std::runtime_error("Bad mass number in nuclide: '" + s + "'"); - - // Small element->Z map (extend as needed). Tell me if you have lots of elements beyond these. + if (A <= 0) { + throw cyclus::ValueError("Bad mass number in nuclide: '" + s + "'"); + } + // example of isotopes definision till I add the actual ones, or find out how to use materials class static const std::unordered_map Z = { - {"H",1},{"He",2},{"Li",3},{"Be",4},{"B",5},{"C",6},{"N",7},{"O",8},{"F",9},{"Ne",10}, - {"Na",11},{"Mg",12},{"Al",13},{"Si",14},{"P",15},{"S",16},{"Cl",17},{"Ar",18}, - {"K",19},{"Ca",20},{"Sc",21},{"Ti",22},{"V",23},{"Cr",24},{"Mn",25},{"Fe",26},{"Co",27},{"Ni",28},{"Cu",29},{"Zn",30}, - {"Ga",31},{"Ge",32},{"As",33},{"Se",34},{"Br",35},{"Kr",36}, - {"Rb",37},{"Sr",38},{"Y",39},{"Zr",40},{"Nb",41},{"Mo",42},{"Tc",43},{"Ru",44},{"Rh",45},{"Pd",46},{"Ag",47},{"Cd",48}, - {"In",49},{"Sn",50},{"Sb",51},{"Te",52},{"I",53},{"Xe",54}, - {"Cs",55},{"Ba",56},{"La",57},{"Ce",58},{"Pr",59},{"Nd",60},{"Pm",61},{"Sm",62},{"Eu",63},{"Gd",64},{"Tb",65},{"Dy",66}, - {"Ho",67},{"Er",68},{"Tm",69},{"Yb",70},{"Lu",71},{"Hf",72},{"Ta",73},{"W",74},{"Re",75},{"Os",76},{"Ir",77},{"Pt",78},{"Au",79},{"Hg",80}, - {"Tl",81},{"Pb",82},{"Bi",83},{"Po",84},{"At",85},{"Rn",86}, - {"Fr",87},{"Ra",88},{"Ac",89},{"Th",90},{"Pa",91},{"U",92},{"Np",93},{"Pu",94},{"Am",95},{"Cm",96} - }; - - auto it = Z.find(sym); - if (it == Z.end()) throw std::runtime_error("Unknown element symbol in nuclide: '" + s + "' (parsed '" + sym + "')"); + {"H", 1}, {"He", 2}, {"Li", 3}, {"Be", 4}, {"B", 5}, {"C", 6}, + }; + + std::unordered_map::const_iterator it = Z.find(sym); + if (it == Z.end()) { + throw cyclus::ValueError( + "Unknown element symbol in nuclide: '" + s + "'"); + } int z = it->second; - int zzaaam = z*10000 + A*10 + 0; // m=0 + int zzaaam = z * 10000 + A * 10; return zzaaam; } -// ---------- Cyclus boilerplate ---------- -#pragma cyclus impl \ No newline at end of file + + +extern "C" cyclus::Agent* ConstructUSInventory(cyclus::Context* ctx) { + return new USInventory(ctx); +} + +} // namespace einstein \ No newline at end of file diff --git a/src/us_inventory.h b/src/us_inventory.h index 47bf4c9..28fef53 100644 --- a/src/us_inventory.h +++ b/src/us_inventory.h @@ -1,4 +1,5 @@ -#pragma once +#ifndef EINSTEIN_US_INVENTORY_H_ +#define EINSTEIN_US_INVENTORY_H_ #include #include @@ -6,57 +7,119 @@ #include "cyclus.h" #include "cyclus_facility.h" +#include "einstein_version.h" + +namespace einstein { class USInventory : public cyclus::Facility { public: - USInventory(cyclus::Context* ctx); + explicit USInventory(cyclus::Context* ctx); virtual ~USInventory(); - // --- Cyclus hooks --- - virtual void EnterNotify(); - virtual std::string str(); - - // --- DRE (Material supplier) --- - virtual void GetMatlBids(cyclus::CommodMap::type& commod_requests, - cyclus::BidPortfolio::type& bids); + #pragma cyclus decl - virtual void GetMatlTrades(const std::vector< cyclus::Trade >& trades, - std::vector< std::pair< cyclus::Trade, - cyclus::Material::Ptr > >& responses); + virtual void InitFrom(USInventory* m); + virtual void InitFrom(cyclus::QueryableBackend* b); - // --- Configurable parameters (set via XML) --- - #pragma cyclus var {"tooltip":"Commodity this facility supplies (e.g., pwr_snf)."} + /// Cyclus hooks + virtual void EnterNotify(); + virtual std::string str(); + virtual void Tick(); + virtual void Tock(); + virtual std::string version() { return EINSTEIN_VERSION; } + + /// Material supplier interface + virtual void GetMatlBids( + cyclus::CommodMap::type& commod_requests, + cyclus::BidPortfolio::type& bids); + + virtual void GetMatlTrades( + const std::vector >& trades, + std::vector, + cyclus::Material::Ptr> >& responses); + + #pragma cyclus note {"doc": "USInventory is a source-like facility that loads " + "spent nuclear fuel inventory data at initialization " + "and supplies material to other facilities on request. " + "It does not accept incoming commodities."} + + /// Output commodity + #pragma cyclus var {"tooltip":"Commodity supplied by this facility.", \ + "doc":"Commodity name offered to requesting facilities.", \ + "uilabel":"Output Commodity", \ + "uitype":"outcommodity"} std::string outcommod; - #pragma cyclus var {"tooltip":"Path to assemblies.csv"} + /// File containing assembly inventory data + #pragma cyclus var {"tooltip":"Path to assemblies CSV file."} std::string assemblies_file; - #pragma cyclus var {"tooltip":"Path to composition.csv"} + /// File containing isotopic composition data + #pragma cyclus var {"tooltip":"Path to composition CSV file."} std::string composition_file; - #pragma cyclus var {"default":1e99, "tooltip":"Max kg this facility can supply per timestep."} + /// Maximum mass supplied per timestep + #pragma cyclus var {"default": 1e99, \ + "tooltip":"Maximum mass supplied per timestep (kg).", \ + "units":"kg"} double throughput_kg; - #pragma cyclus var {"default":true, "tooltip":"Allow partial fulfillment (mass-based)."} + /// Whether partial requests may be fulfilled + #pragma cyclus var {"default": true, \ + "tooltip":"Allow partial fulfillment of requests."} bool allow_partial; - private: + /// Bin selection policy + #pragma cyclus var {"default":"first", \ + "tooltip":"Bin selection policy: first, older, newer, highest_burnup, lowest_burnup, highest_enrichment, lowest_enrichment."} + std::string selection_policy; + + protected: struct Bin { std::string assembly_id; - double available_kg = 0.0; // remaining mass + double available_kg; cyclus::Composition::Ptr comp; + + int discharge_date; + double burnup; + double enrichment; + + Bin() + : assembly_id(""), + available_kg(0.0), + comp(), + discharge_date(0), + burnup(0.0), + enrichment(0.0) {} }; - // Storage: bins in FIFO order + /// Inventory bins loaded from the database std::vector bins_; - // Fast lookup: assembly_id -> index in bins_ + /// Fast lookup from assembly id to bin index std::unordered_map idx_; - // Helper: CSV loading + /// Total available mass in all bins + double total_inventory_kg_; + + /// Helper methods for loading data void LoadAssembliesCSV_(const std::string& path); void LoadCompositionCSV_(const std::string& path); - // Helper: Convert nuclide string (e.g., "U235") to nuc id (zzaaam) + /// Helper for choosing a bin according to policy + size_t ChooseBin_(double req_qty, bool full_only) const; + + /// Helper for nuclide-name parsing int NucIdFromString_(const std::string& s) const; -}; \ No newline at end of file + + friend class USInventoryTest; + + private: + // Code Injection + #include "toolkit/matl_sell_policy.cycpp.h" + #include "toolkit/position.cycpp.h" +}; + +} // namespace einstein + +#endif // EINSTEIN_US_INVENTORY_H_ \ No newline at end of file