diff --git a/.gitignore b/.gitignore index 2a54233..e8c7778 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,12 @@ cmake-build-*/ example/ #cyclus outputs -.sqlite +*.sqlite + +#data files +*.csv +*.json +*.jsonl # CMake files CMakeCache.txt @@ -29,10 +34,11 @@ __pycache__/ # macOS .DS_Store +*.DS_Store # VSCode .vscode/ # Conda .env -.conda \ No newline at end of file +.conda diff --git a/examples/trial1.xml b/examples/trial1.xml new file mode 100644 index 0000000..3490946 --- /dev/null +++ b/examples/trial1.xml @@ -0,0 +1,83 @@ + + + + + 500 + 1 + 2024 +
2628000
+ 1 +
+ + + + einstein + us_inventory + + + cycamore + Sink + + + agents + NullRegion + + + agents + NullInst + + + + + + us_inventory + + + spent_fuel + ./assemblies.csv + ./composition.csv + + + + + + Sink + + + + spent_fuel + + 10000 + + + + + + SingleRegion + + + SingleInstitution + + + us_inventory + 1 + + + Sink + 1 + + + + + + + + commod_recipe + mass + + 010010000 + 1 + + + +
\ No newline at end of file diff --git a/examples/trial2.xml b/examples/trial2.xml new file mode 100644 index 0000000..a69393e --- /dev/null +++ b/examples/trial2.xml @@ -0,0 +1,82 @@ + + + + + 10 + 1 + 2000 + 1 + + + + + einstein + us_inventory + + + cycamore + Sink + + + agents + NullRegion + + + agents + NullInst + + + + + us_inventory + + + commodity + ./assemblies.csv + ./composition.csv + 1000 + + + + + + Sink + + + + commodity + + 1 + + + + + + SingleRegion + + + SingleInstitution + + + us_inventory + 1 + + + Sink + 1 + + + + + + + + commod_recipe + mass + + 010010000 + 1 + + + + diff --git a/src/us_inventory.cc b/src/us_inventory.cc index 46e610e..522e50a 100644 --- a/src/us_inventory.cc +++ b/src/us_inventory.cc @@ -1,24 +1,547 @@ #include "us_inventory.h" +#include +#include +#include +#include +#include +#include + +//#include "pyne/nucname.h" + namespace einstein { -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -us_inventory::us_inventory(cyclus::Context* ctx) : cyclus::Facility(ctx) {} +us_inventory::us_inventory(cyclus::Context* ctx) + : cyclus::Facility(ctx), + total_inventory_kg_(0.0) {} + +us_inventory::~us_inventory() {} + +void us_inventory::InitFrom(us_inventory* m) { + #pragma cyclus impl initfromcopy einstein::us_inventory + cyclus::toolkit::CommodityProducer::Copy(m); +} + +void us_inventory::InitFrom(cyclus::QueryableBackend* b) { + #pragma cyclus impl initfromdb einstein::us_inventory + namespace tk = cyclus::toolkit; + tk::CommodityProducer::Add(tk::Commodity(outcommod), + tk::CommodInfo(throughput_kg, throughput_kg)); +} -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - std::string us_inventory::str() { - return Facility::str(); + std::stringstream ss; + ss << cyclus::Facility::str() + << " us_inventory(outcommod=" << outcommod + << ", bins=" << bins_.size() + << ", total_inventory_kg=" << total_inventory_kg_ + << ", throughput_kg=" << throughput_kg + << ", selection_policy=" << selection_policy + << ")"; + return ss.str(); +} + +void us_inventory::EnterNotify() { + cyclus::Facility::EnterNotify(); + + if (outcommod.empty()) { + throw cyclus::ValueError("us_inventory: outcommod is required."); + } + if (assemblies_file.empty() || composition_file.empty()) { + throw cyclus::ValueError( + "us_inventory: assemblies_file and composition_file are required."); + } + + // Validating the selection_policy early so the user gets a clear error. + static const char* valid_policies[] = { + "first", "older", "newer", + "highest_burnup", "lowest_burnup", + "highest_enrichment", "lowest_enrichment"}; + bool policy_ok = false; + for (size_t i = 0; i < 7; ++i) { + if (selection_policy == valid_policies[i]) { policy_ok = true; break; } + } + if (!policy_ok) { + throw cyclus::ValueError( + "us_inventory: unknown selection_policy '" + selection_policy + "'. " + "Valid options: first, older, newer, highest_burnup, lowest_burnup, " + "highest_enrichment, lowest_enrichment."); + } + + bins_.clear(); + idx_.clear(); + total_inventory_kg_ = 0.0; + + LoadAssembliesCSV_(assemblies_file); + LoadCompositionCSV_(composition_file); + + + if (!remaining_kg_.empty()) { + if (remaining_kg_.size() != bins_.size()) { + throw cyclus::ValueError( + "us_inventory: persisted remaining_kg_ length does not match " + "the number of rows in assemblies_file. Did the CSV change " + "after a checkpoint?"); + } + total_inventory_kg_ = 0.0; + for (size_t i = 0; i < bins_.size(); ++i) { + bins_[i].available_kg = remaining_kg_[i]; + total_inventory_kg_ += remaining_kg_[i]; + } + } else { + // First build: snapshot the initial CSV masses. + remaining_kg_.resize(bins_.size()); + for (size_t i = 0; i < bins_.size(); ++i) { + remaining_kg_[i] = bins_[i].available_kg; + } + } + + for (size_t i = 0; i < bins_.size(); ++i) { + if (bins_[i].comp == NULL) { + throw cyclus::ValueError( + "us_inventory: missing composition for assembly_id=" + + bins_[i].assembly_id); + } + } +} + +std::set::Ptr> +us_inventory::GetMatlBids( + cyclus::CommodMap::type& commod_requests) { + using cyclus::BidPortfolio; + using cyclus::CapacityConstraint; + using cyclus::Material; + using cyclus::Request; + + std::set::Ptr> ports; + + if (commod_requests.count(outcommod) == 0) { + return ports; + } + + double max_qty = std::min(total_inventory_kg_, throughput_kg); + if (max_qty <= cyclus::eps()) { + return ports; + } + + cyclus::CompMap blended; + double blend_total = 0.0; + for (size_t i = 0; i < bins_.size(); ++i) { + const Bin& b = bins_[i]; + if (b.available_kg <= cyclus::eps() || b.comp == NULL) continue; + const cyclus::CompMap& cm = b.comp->mass(); + for (cyclus::CompMap::const_iterator it = cm.begin(); it != cm.end(); ++it) { + blended[it->first] += it->second * b.available_kg; + } + blend_total += b.available_kg; + } + + if (blend_total <= cyclus::eps()) { + return ports; + } + + for (cyclus::CompMap::iterator it = blended.begin(); + it != blended.end(); ++it) { + it->second /= blend_total; + } + + cyclus::Composition::Ptr bid_comp = + cyclus::Composition::CreateFromMass(blended); + + BidPortfolio::Ptr port(new BidPortfolio()); + std::vector*>& requests = commod_requests[outcommod]; + + for (size_t i = 0; i < requests.size(); ++i) { + Request* req = requests[i]; + double qty = std::min(req->target()->quantity(), max_qty); + if (qty <= cyclus::eps()) continue; + + // When partial fulfillment is forbidden, only bid if we can satisfy the + // full request. + if (!allow_partial && qty < req->target()->quantity()) continue; + + Material::Ptr offer = Material::CreateUntracked(qty, bid_comp); + port->AddBid(req, offer, this); + } + + CapacityConstraint cc(max_qty); + port->AddConstraint(cc); + ports.insert(port); + return ports; +} + +// Choosing a Bin/ SNF assembly + + +size_t us_inventory::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 <= cyclus::eps()) continue; + if (full_only && b.available_kg < req_qty) continue; + + // First eligible bin — always accept it as the initial candidate. + if (best == bins_.size()) { + best = i; + continue; + } + + const Bin& cur = bins_[best]; + + if (selection_policy == "first") { + // Keep the first eligible bin; no further comparison needed. + 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; + } + } + + return best; } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void us_inventory::Tick() {} -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -void us_inventory::Tock() {} +void us_inventory::GetMatlTrades( + const std::vector >& trades, + std::vector, + cyclus::Material::Ptr> >& responses) { + double remaining_throughput = throughput_kg; + + for (size_t t = 0; t < trades.size(); ++t) { + const cyclus::Trade& tr = trades[t]; + double req_qty = tr.amt; + + if (req_qty <= cyclus::eps()) continue; + if (remaining_throughput <= cyclus::eps()) break; + if (total_inventory_kg_ <= cyclus::eps()) break; + + // --- Determine how much to send and which bin to draw from ------------- + + // First try to find a single bin that can satisfy the full request. + size_t chosen_i = ChooseBin_(req_qty, /*full_only=*/true); + double actual = 0.0; + + if (chosen_i != bins_.size()) { + // A bin with enough material was found. + actual = std::min(req_qty, remaining_throughput); + if (!allow_partial && bins_[chosen_i].available_kg < req_qty) continue; + } else { + // No single bin can fully satisfy the request. + if (!allow_partial) continue; + + // Partial mode: pick the best bin that has anything at all. + chosen_i = ChooseBin_(req_qty, /*full_only=*/false); + if (chosen_i == bins_.size()) continue; + + actual = std::min({req_qty, + bins_[chosen_i].available_kg, + remaining_throughput}); + } + + if (actual <= cyclus::eps()) continue; + + // --- Draw from the chosen bin + std::vector draw_kg(bins_.size(), 0.0); + draw_kg[chosen_i] = actual; + + Bin& b = bins_[chosen_i]; + b.available_kg -= actual; + total_inventory_kg_ -= actual; + remaining_throughput -= actual; + + remaining_kg_[chosen_i] = b.available_kg; + + cyclus::Composition::Ptr comp = BlendedComp_(draw_kg); + cyclus::Material::Ptr mat = + cyclus::Material::CreateUntracked(actual, comp); + + responses.push_back(std::make_pair(tr, mat)); + + LOG(cyclus::LEV_INFO5, "us_inventory") + << prototype() << " sent " << actual << " kg of " << outcommod + << " from bin " << b.assembly_id + << " (policy=" << selection_policy << ")"; + } +} + +// BlendedComp_ + +cyclus::Composition::Ptr us_inventory::BlendedComp_( + const std::vector& draw_kg) const { + cyclus::CompMap blended; + double total = 0.0; + + for (size_t i = 0; i < bins_.size(); ++i) { + if (draw_kg[i] <= cyclus::eps() || bins_[i].comp == NULL) continue; + const cyclus::CompMap& cm = bins_[i].comp->mass(); + for (cyclus::CompMap::const_iterator it = cm.begin(); + it != cm.end(); ++it) { + blended[it->first] += it->second * draw_kg[i]; + } + total += draw_kg[i]; + } + + if (total > cyclus::eps()) { + for (cyclus::CompMap::iterator it = blended.begin(); + it != blended.end(); ++it) { + it->second /= total; + } + } + + return cyclus::Composition::CreateFromMass(blended); +} + +// CSV helpers + +namespace { + +std::vector SplitCSVLine(const std::string& line) { + std::vector out; + std::stringstream ss(line); + std::string item; + while (std::getline(ss, item, ',')) { + 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; +} + +} + +void us_inventory::LoadAssembliesCSV_(const std::string& path) { + std::ifstream f(path.c_str()); + if (!f) throw cyclus::ValueError("us_inventory: cannot open " + path); + + std::string header; + if (!std::getline(f, header)) + throw cyclus::ValueError("us_inventory: empty file " + path); + + std::vector cols = SplitCSVLine(header); + + // Map column names to indices + 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 = static_cast(i); + else if (cols[i] == "total_mass_kg") i_mass = static_cast(i); + else if (cols[i] == "count") i_count = static_cast(i); + else if (cols[i] == "discharge_date")i_date = static_cast(i); + else if (cols[i] == "burnup") i_bu = static_cast(i); + else if (cols[i] == "enrichment") i_enr = static_cast(i); + } + + if (i_id < 0 || i_mass < 0) { + throw cyclus::ValueError( + "us_inventory: assemblies.csv must contain assembly_id and " + "total_mass_kg columns."); + } + + std::string line; + while (std::getline(f, line)) { + if (line.empty()) continue; + std::vector v = SplitCSVLine(line); + if (static_cast(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 < static_cast(v.size()) && + !v[i_count].empty()) { + count = std::stod(v[i_count]); + } + + b.available_kg = mass * count; + + if (i_date >= 0 && i_date < static_cast(v.size()) && + !v[i_date].empty()) { + b.discharge_date = std::stod(v[i_date].substr(0, 4)); + } + + if (i_bu >= 0 && i_bu < static_cast(v.size()) && + !v[i_bu].empty()) { + b.burnup = std::stod(v[i_bu]); + } + + if (i_enr >= 0 && i_enr < static_cast(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 cyclus::ValueError("us_inventory: no rows loaded from " + path); +} + +void us_inventory::LoadCompositionCSV_(const std::string& path) { + std::ifstream f(path.c_str()); + if (!f) throw cyclus::ValueError("us_inventory: cannot open " + path); + + std::string header; + if (!std::getline(f, header)) + throw cyclus::ValueError("us_inventory: empty file " + path); + + std::vector cols = SplitCSVLine(header); + + int i_id = -1; + int i_nuc = -1; + int i_frac = -1; + + for (size_t i = 0; i < cols.size(); ++i) { + if (cols[i] == "assembly_id") i_id = static_cast(i); + else if (cols[i] == "nuclide") i_nuc = static_cast(i); + else if (cols[i] == "mass_fraction") i_frac = static_cast(i); + } + + if (i_id < 0 || i_nuc < 0 || i_frac < 0) { + throw cyclus::ValueError( + "us_inventory: composition.csv must contain assembly_id, nuclide, " + "and mass_fraction columns."); + } + + std::unordered_map compmaps; + + std::string line; + while (std::getline(f, line)) { + if (line.empty()) continue; + std::vector v = SplitCSVLine(line); + if (static_cast(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; + + int nid = NucIdFromString_(nuc); + compmaps[aid][nid] += frac; + } + + 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 — I should try using PyNE instead of ZZAAAM. +// --------------------------------------------------------------------------- + +int us_inventory::NucIdFromString_(const std::string& s) const { + std::string t = s; + + 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 + "'"); + } + + size_t pos = 0; + while (pos < t.size() && std::isalpha(static_cast(t[pos]))) { + pos++; + } + + if (pos == 0 || pos == t.size()) { + throw std::runtime_error("Bad nuclide string: '" + s + "'"); + } + + std::string sym = t.substr(0, pos); + std::string a_str = t.substr(pos); + + // Strip trailing metastable indicator ('m') + // e.g. "108m" -> "108" + size_t m_pos = a_str.find_first_not_of("0123456789"); + if (m_pos != std::string::npos) { + a_str = a_str.substr(0, m_pos); // keep only the digits + } + + 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 + "'"); + } + + 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}, {"Bk", 97}, {"Cf", 98}, {"Es", 99}, {"Fm", 100}, + {"Md", 101},{"No", 102},{"Lr", 103}}; + + auto it = Z.find(sym); + + if (it == Z.end()) { + throw std::runtime_error( + "Unknown element symbol in nuclide: '" + s + "' parsed as '" + + sym + "'"); + } + + int z = it->second; + int zzaaam = z * 10000000 + A * 10000; + + return zzaaam; +} -// WARNING! Do not change the following this function!!! This enables your -// archetype to be dynamically loaded and any alterations will cause your -// archetype to fail. extern "C" cyclus::Agent* Constructus_inventory(cyclus::Context* ctx) { return new us_inventory(ctx); } diff --git a/src/us_inventory.h b/src/us_inventory.h index cab97a4..d205308 100644 --- a/src/us_inventory.h +++ b/src/us_inventory.h @@ -1,66 +1,180 @@ -#ifndef CYCLUS_EINSTEIN_US_INVENTORY_H_ -#define CYCLUS_EINSTEIN_US_INVENTORY_H_ +#ifndef EINSTEIN_SRC_US_INVENTORY_H_ +#define EINSTEIN_SRC_US_INVENTORY_H_ +#include #include +#include +#include #include "cyclus.h" +#pragma cyclus exec from cyclus.system import CY_LARGE_DOUBLE + namespace einstein { -/// @class us_inventory -/// -/// This Facility is intended -/// as a skeleton to guide the implementation of new Facility -/// agents. -/// The us_inventory class inherits from the Facility class and is -/// dynamically loaded by the Agent class when requested. -/// -/// @section intro Introduction -/// Place an introduction to the agent here. -/// -/// @section agentparams Agent Parameters -/// Place a description of the required input parameters which define the -/// agent implementation. -/// -/// @section optionalparams Optional Parameters -/// Place a description of the optional input parameters to define the -/// agent implementation. -/// -/// @section detailed Detailed Behavior -/// Place a description of the detailed behavior of the agent. Consider -/// describing the behavior at the tick and tock as well as the behavior -/// upon sending and receiving materials and messages. -class us_inventory : public cyclus::Facility { +/// This facility represents a supply inventory of used nuclear fuel assemblies. +/// It reads assembly masses and isotopic compositions from CSV files, stores +/// them internally as inventory bins, and offers material on a configured +/// output commodity. Material is supplied from the available bins subject to a +/// per-timestep throughput limit. +class us_inventory : public cyclus::Facility, + public cyclus::toolkit::CommodityProducer { + public: - /// Constructor for us_inventory Class - /// @param ctx the cyclus context for access to simulation-wide parameters - explicit us_inventory(cyclus::Context* ctx); + us_inventory(cyclus::Context* ctx); + virtual ~us_inventory(); + + #pragma cyclus note { \ + "doc": "This facility represents a supply inventory of used nuclear fuel " \ + "assemblies. It reads assembly masses and isotopic compositions " \ + "from CSV files, stores them internally as inventory bins, and " \ + "offers material on a configured output commodity. Material is " \ + "supplied from the available bins subject to a per-timestep " \ + "throughput limit.", \ + } - /// The Prime Directive - /// Generates code that handles all input file reading and restart operations - /// (e.g., reading from the database, instantiating a new object, etc.). - /// @warning The Prime Directive must have a space before it! (A fix will be - /// in 2.0 ^TM) + #pragma cyclus def clone + #pragma cyclus def schema + #pragma cyclus def annotations + #pragma cyclus def infiletodb + #pragma cyclus def snapshot + #pragma cyclus def snapshotinv + #pragma cyclus def initinv - #pragma cyclus + virtual void InitFrom(us_inventory* m); + virtual void InitFrom(cyclus::QueryableBackend* b); - #pragma cyclus note {"doc": "A stub facility is provided as a skeleton " \ - "for the design of new facility agents."} + virtual void Tick() {}; + + virtual void Tock() {}; - /// A verbose printer for the us_inventory virtual std::string str(); + virtual void EnterNotify(); + + virtual std::set::Ptr> + GetMatlBids(cyclus::CommodMap::type& commod_requests); + + virtual void GetMatlTrades( + const std::vector >& trades, + std::vector, + cyclus::Material::Ptr> >& responses); + + private: + // Cyclus state variables + + #pragma cyclus var { \ + "tooltip": "Commodity this facility supplies.", \ + "doc": "Output commodity on which the us_inventory facility offers " \ + "used nuclear fuel material.", \ + "uilabel": "Output Commodity", \ + "uitype": "outcommodity", \ + } + std::string outcommod; + + #pragma cyclus var { \ + "tooltip": "Path to assemblies CSV file.", \ + "doc": "Path to a CSV file containing assembly IDs and total available " \ + "masses. Required columns: assembly_id, total_mass_kg. " \ + "Optional columns: count, discharge_date, burnup, enrichment.", \ + "uilabel": "Assemblies File", \ + } + std::string assemblies_file; + + #pragma cyclus var { \ + "tooltip": "Path to composition CSV file.", \ + "doc": "Path to a CSV file containing isotopic mass fractions for each " \ + "assembly. Required columns: assembly_id, nuclide, mass_fraction.", \ + "uilabel": "Composition File", \ + } + std::string composition_file; + + #pragma cyclus var { \ + "default": CY_LARGE_DOUBLE, \ + "tooltip": "Maximum material mass this facility can supply per time step.", \ + "units": "kg/(time step)", \ + "uilabel": "Maximum Throughput", \ + "uitype": "range", \ + "range": [0.0, CY_LARGE_DOUBLE], \ + "doc": "Amount of commodity that can be supplied at each time step.", \ + } + double throughput_kg; - /// The handleTick function specific to the us_inventory. - /// @param time the time of the tick - virtual void Tick(); + #pragma cyclus var { \ + "default": True, \ + "tooltip": "Allow partial fulfillment of material requests.", \ + "doc": "If true, the facility may partially satisfy a material trade when " \ + "the requested mass exceeds the available mass or remaining " \ + "throughput. If false, trades are only fulfilled when the full " \ + "requested quantity can be supplied.", \ + "uilabel": "Allow Partial Fulfillment", \ + } + bool allow_partial; - /// The handleTick function specific to the us_inventory. - /// @param time the time of the tock - virtual void Tock(); + #pragma cyclus var { \ + "default": "first", \ + "tooltip": "Policy used to select which bin to draw from.", \ + "doc": "Controls which assembly bin is chosen when fulfilling a trade. " \ + "Options: " \ + "'first' - always pick the earliest bin with available material; " \ + "'older' - prefer the bin with the smallest discharge_date; " \ + "'newer' - prefer the bin with the largest discharge_date; " \ + "'highest_burnup' - prefer the bin with the highest burnup; " \ + "'lowest_burnup' - prefer the bin with the lowest burnup; " \ + "'highest_enrichment' - prefer the bin with the highest initial enrichment; " \ + "'lowest_enrichment' - prefer the bin with the lowest initial enrichment.", \ + "uilabel": "Bin Selection Policy", \ + } + std::string selection_policy; - // And away we go! + #pragma cyclus var { \ + "default": [], \ + "doc": "Persisted remaining mass (kg) for each assembly bin. " \ + "Managed internally — do not set by hand.", \ + "uilabel": "Remaining Masses (internal)", \ + } + std::vector remaining_kg_; + + // --------------------------------------------------------------------------- + // Internal (non-persisted) data structures + // --------------------------------------------------------------------------- + + /// One entry per assembly (or assembly group) read from the CSV. + struct Bin { + std::string assembly_id; + double available_kg = 0.0; + double discharge_date = 0.0; // optional: used by older/newer policy + double burnup = 0.0; // optional: used by highest/lowest_burnup + double enrichment = 0.0; // optional: used by highest/lowest_enrichment + cyclus::Composition::Ptr comp; + }; + + std::vector bins_; + std::unordered_map idx_; // assembly_id -> bins_ index + double total_inventory_kg_; // running total for fast checks + + // --------------------------------------------------------------------------- + // Private helpers + // --------------------------------------------------------------------------- + + /// Return the index of the best bin for a trade of req_qty kg according to + /// selection_policy. If full_only is true only bins that can fully satisfy + /// req_qty are considered. Returns bins_.size() if no suitable bin is found. + size_t ChooseBin_(double req_qty, bool full_only) const; + + /// Build a Composition that is a mass-weighted blend of the bins drawn from. + /// draw_kg[i] is the mass drawn from bins_[i]. + cyclus::Composition::Ptr BlendedComp_( + const std::vector& draw_kg) const; + + void LoadAssembliesCSV_(const std::string& path); + void LoadCompositionCSV_(const std::string& path); + + /// Convert a nuclide string (e.g. "U-235", "U235", "92235") to a PyNE id. + int NucIdFromString_(const std::string& s) const; }; } // namespace einstein -#endif // CYCLUS_EINSTEIN_US_INVENTORY_H_ + +#endif // EINSTEIN_SRC_US_INVENTORY_H_ +