| | | 1 | | // Copyright 2026 Digital Holography Foundation |
| | | 2 | | // |
| | | 3 | | // Licensed under the Apache License, Version 2.0 (the "License"); |
| | | 4 | | // you may not use this file except in compliance with the License. |
| | | 5 | | // You may obtain a copy of the License at |
| | | 6 | | // |
| | | 7 | | // http://www.apache.org/licenses/LICENSE-2.0 |
| | | 8 | | // |
| | | 9 | | // Unless required by applicable law or agreed to in writing, software |
| | | 10 | | // distributed under the License is distributed on an "AS IS" BASIS, |
| | | 11 | | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| | | 12 | | // See the License for the specific language governing permissions and |
| | | 13 | | // limitations under the License. |
| | | 14 | | |
| | | 15 | | #include "holonp/slice.hh" |
| | | 16 | | |
| | | 17 | | #include <algorithm> |
| | | 18 | | #include <numeric> |
| | | 19 | | #include <stdexcept> |
| | | 20 | | #include <string> |
| | | 21 | | |
| | | 22 | | namespace holonp { |
| | | 23 | | |
| | | 24 | | // ------------------------------------------------------------------------------------------------- |
| | | 25 | | // JSON serialization |
| | | 26 | | // ------------------------------------------------------------------------------------------------- |
| | | 27 | | |
| | 0 | 28 | | void to_json(nlohmann::json &j, const SliceRange &s) { |
| | 0 | 29 | | j = nlohmann::json{ |
| | | 30 | | {"start", s.start.has_value() ? nlohmann::json(*s.start) : nlohmann::json(nullptr)}, |
| | | 31 | | {"stop", s.stop.has_value() ? nlohmann::json(*s.stop) : nlohmann::json(nullptr)}, |
| | | 32 | | {"step", s.step}, |
| | | 33 | | }; |
| | 0 | 34 | | } |
| | | 35 | | |
| | 1 | 36 | | void from_json(const nlohmann::json &j, SliceRange &s) { |
| | 1 | 37 | | if (j.contains("start") && !j.at("start").is_null()) { |
| | 1 | 38 | | s.start = j.at("start").get<std::int64_t>(); |
| | 1 | 39 | | } else { |
| | 1 | 40 | | s.start = std::nullopt; |
| | | 41 | | } |
| | | 42 | | |
| | 1 | 43 | | if (j.contains("stop") && !j.at("stop").is_null()) { |
| | 1 | 44 | | s.stop = j.at("stop").get<std::int64_t>(); |
| | 1 | 45 | | } else { |
| | 1 | 46 | | s.stop = std::nullopt; |
| | | 47 | | } |
| | | 48 | | |
| | | 49 | | // Default step to 1 if not present |
| | 1 | 50 | | s.step = j.value("step", 1); |
| | 1 | 51 | | } |
| | | 52 | | |
| | 0 | 53 | | void to_json(nlohmann::json &j, const SliceItem &s) { |
| | 0 | 54 | | std::visit( |
| | | 55 | | [&](auto &&arg) { |
| | | 56 | | using T = std::decay_t<decltype(arg)>; |
| | | 57 | | if constexpr (std::is_same_v<T, std::int64_t>) { |
| | | 58 | | j = arg; // Serialize as integer |
| | | 59 | | } else { |
| | | 60 | | j = arg; // Serialize as SliceRange object |
| | | 61 | | } |
| | | 62 | | }, |
| | | 63 | | s); |
| | 0 | 64 | | } |
| | | 65 | | |
| | 1 | 66 | | void from_json(const nlohmann::json &j, SliceItem &s) { |
| | 1 | 67 | | if (j.is_number_integer()) { |
| | | 68 | | // If it's a number, it's a direct index (e.g. 5) |
| | 1 | 69 | | s = j.get<std::int64_t>(); |
| | 1 | 70 | | } else { |
| | | 71 | | // If it's an object (or null/empty), treat as SliceRange |
| | 1 | 72 | | s = j.get<SliceRange>(); |
| | | 73 | | } |
| | 1 | 74 | | } |
| | | 75 | | |
| | 0 | 76 | | void to_json(nlohmann::json &j, const SliceSettings &s) { |
| | 0 | 77 | | j = nlohmann::json{{"slices", s.slices}}; |
| | 0 | 78 | | } |
| | | 79 | | |
| | 1 | 80 | | void from_json(const nlohmann::json &j, SliceSettings &s) { j.at("slices").get_to(s.slices); } |
| | | 81 | | |
| | | 82 | | namespace { |
| | | 83 | | |
| | | 84 | | // ------------------------------------------------------------------------------------------------- |
| | | 85 | | // Helpers |
| | | 86 | | // ------------------------------------------------------------------------------------------------- |
| | | 87 | | |
| | | 88 | | constexpr int kMaxNDim = 16; |
| | | 89 | | |
| | 1 | 90 | | inline void check(bool cond, const std::string &msg) { |
| | 1 | 91 | | if (!cond) { |
| | 0 | 92 | | throw std::invalid_argument("Slice: " + msg); |
| | | 93 | | } |
| | 1 | 94 | | } |
| | | 95 | | |
| | | 96 | | struct NormalizedSlice { |
| | | 97 | | std::int64_t start; |
| | | 98 | | std::int64_t stop; // exclusive |
| | | 99 | | std::int64_t step; // > 0 |
| | | 100 | | }; |
| | | 101 | | |
| | | 102 | | // Helper: Validate and Normalize a single Integer Index |
| | 1 | 103 | | inline std::int64_t normalize_index(std::int64_t idx, std::int64_t dim) { |
| | 1 | 104 | | check(dim > 0, "cannot index into 0-sized dimension"); |
| | | 105 | | |
| | | 106 | | // Handle negative wrapping |
| | 1 | 107 | | if (idx < 0) { |
| | 0 | 108 | | idx += dim; |
| | | 109 | | } |
| | | 110 | | |
| | | 111 | | // Strict Bound Checking |
| | 1 | 112 | | if (idx < 0 || idx >= dim) { |
| | 1 | 113 | | throw std::out_of_range("Slice index " + std::to_string(idx) + |
| | | 114 | | " is out of bounds for dimension size " + std::to_string(dim)); |
| | | 115 | | } |
| | 1 | 116 | | return idx; |
| | 1 | 117 | | } |
| | | 118 | | |
| | | 119 | | // Helper: Validate and Normalize a Slice Range |
| | 1 | 120 | | inline NormalizedSlice normalize_slice_range(const SliceRange &s, std::int64_t dim) { |
| | 1 | 121 | | check(dim >= 0, "invalid dimension"); |
| | | 122 | | |
| | 1 | 123 | | const auto step = s.step; |
| | 1 | 124 | | check(step > 0, "only positive step is supported for now"); |
| | | 125 | | |
| | 1 | 126 | | std::int64_t start = s.start.value_or(0); |
| | 1 | 127 | | std::int64_t stop = s.stop.value_or(dim); |
| | | 128 | | |
| | | 129 | | // 1. Handle negative wrapping |
| | 1 | 130 | | if (start < 0) |
| | 0 | 131 | | start += dim; |
| | 1 | 132 | | if (stop < 0) |
| | 0 | 133 | | stop += dim; |
| | | 134 | | |
| | | 135 | | // 2. Strict Bound Checking (optional, depending on desired strictness vs numpy leniency) |
| | | 136 | | // For safety in this environment, we check bounds strictly relative to 0. |
| | | 137 | | // Note: NumPy usually clamps start/stop, but throws on integer indexing. |
| | | 138 | | // Here we clamp to maintain view safety. |
| | 1 | 139 | | start = std::clamp<std::int64_t>(start, 0, dim); |
| | 1 | 140 | | stop = std::clamp<std::int64_t>(stop, 0, dim); |
| | | 141 | | |
| | 1 | 142 | | return NormalizedSlice{start, stop, step}; |
| | 1 | 143 | | } |
| | | 144 | | |
| | 1 | 145 | | inline std::int64_t out_len(const NormalizedSlice &ns) { |
| | 1 | 146 | | if (ns.start >= ns.stop) { |
| | 0 | 147 | | return 0; |
| | | 148 | | } |
| | 1 | 149 | | const auto span = ns.stop - ns.start; |
| | 1 | 150 | | return (span + ns.step - 1) / ns.step; |
| | 1 | 151 | | } |
| | | 152 | | |
| | | 153 | | // Helper to get consistent BYTES strides |
| | 1 | 154 | | inline std::vector<size_t> ensure_strides(const holoflow::core::TDesc &desc) { |
| | 1 | 155 | | if (!desc.strides.empty()) { |
| | 1 | 156 | | return desc.strides; |
| | | 157 | | } |
| | 0 | 158 | | std::vector<size_t> strides(desc.shape.size()); |
| | 0 | 159 | | size_t acc = holoflow::core::size_of(desc.dtype); |
| | 0 | 160 | | for (int i = static_cast<int>(desc.shape.size()) - 1; i >= 0; --i) { |
| | 0 | 161 | | strides[i] = acc; |
| | 0 | 162 | | acc *= desc.shape[i]; |
| | 0 | 163 | | } |
| | 0 | 164 | | return strides; |
| | 1 | 165 | | } |
| | | 166 | | |
| | | 167 | | // ------------------------------------------------------------------------------------------------- |
| | | 168 | | // Slice task implementation |
| | | 169 | | // ------------------------------------------------------------------------------------------------- |
| | | 170 | | |
| | | 171 | | class Slice : public holoflow::core::ISyncTask { |
| | | 172 | | public: |
| | | 173 | | holoflow::core::OpResult execute(holoflow::core::SyncCtx &ctx) override; |
| | | 174 | | }; |
| | | 175 | | |
| | | 176 | | } // namespace |
| | | 177 | | |
| | 0 | 178 | | holoflow::core::OpResult Slice::execute(holoflow::core::SyncCtx &ctx) { |
| | | 179 | | (void)ctx; |
| | 0 | 180 | | return holoflow::core::OpResult::Ok; |
| | 0 | 181 | | } |
| | | 182 | | |
| | | 183 | | // ------------------------------------------------------------------------------------------------- |
| | | 184 | | // SliceFactory |
| | | 185 | | // ------------------------------------------------------------------------------------------------- |
| | | 186 | | |
| | | 187 | | holoflow::core::InferResult SliceFactory::infer(std::span<const holoflow::core::TDesc> input_descs, |
| | 1 | 188 | | const nlohmann::json &jsettings) const { |
| | 1 | 189 | | check(input_descs.size() == 1, "expected exactly 1 input"); |
| | 1 | 190 | | const auto &idesc = input_descs[0]; |
| | | 191 | | |
| | 1 | 192 | | const int ndim = static_cast<int>(idesc.shape.size()); |
| | 1 | 193 | | check(ndim > 0, "input ndim must be > 0"); |
| | 1 | 194 | | check(ndim <= kMaxNDim, "input ndim too large"); |
| | | 195 | | |
| | 1 | 196 | | const auto in_strides = ensure_strides(idesc); |
| | 1 | 197 | | const auto settings = jsettings.get<SliceSettings>(); |
| | | 198 | | |
| | 1 | 199 | | check(static_cast<int>(settings.slices.size()) == ndim, |
| | | 200 | | "number of slice items must match input ndim"); |
| | | 201 | | |
| | 1 | 202 | | std::vector<size_t> out_shape; |
| | 1 | 203 | | std::vector<size_t> out_strides; |
| | 1 | 204 | | out_shape.reserve(ndim); |
| | 1 | 205 | | out_strides.reserve(ndim); |
| | | 206 | | |
| | | 207 | | // Calculate new Offset relative to current input offset |
| | 1 | 208 | | size_t added_offset_bytes = 0; |
| | | 209 | | |
| | 1 | 210 | | for (int i = 0; i < ndim; ++i) { |
| | 1 | 211 | | const auto &item = settings.slices[i]; |
| | 1 | 212 | | const auto dim_size = static_cast<std::int64_t>(idesc.shape[i]); |
| | 1 | 213 | | const auto dim_stride = in_strides[i]; |
| | | 214 | | |
| | 1 | 215 | | std::visit( |
| | | 216 | | [&](auto &&arg) { |
| | | 217 | | using T = std::decay_t<decltype(arg)>; |
| | | 218 | | |
| | | 219 | | if constexpr (std::is_same_v<T, std::int64_t>) { |
| | | 220 | | // === CASE 1: Integer Index (Dimensionality Reduction) === |
| | | 221 | | // Calculate offset, but do NOT add to out_shape/out_strides |
| | | 222 | | const std::int64_t idx = normalize_index(arg, dim_size); |
| | | 223 | | added_offset_bytes += static_cast<size_t>(idx) * dim_stride; |
| | | 224 | | } else { |
| | | 225 | | // === CASE 2: Slice Range (Preserve Dimension) === |
| | | 226 | | const auto ns = normalize_slice_range(arg, dim_size); |
| | | 227 | | |
| | | 228 | | // Add offset for the start of the slice |
| | | 229 | | added_offset_bytes += static_cast<size_t>(ns.start) * dim_stride; |
| | | 230 | | |
| | | 231 | | // Push new dimension shape and stride |
| | | 232 | | out_shape.push_back(static_cast<size_t>(out_len(ns))); |
| | | 233 | | out_strides.push_back(dim_stride * static_cast<size_t>(ns.step)); |
| | | 234 | | } |
| | | 235 | | }, |
| | | 236 | | item); |
| | 1 | 237 | | } |
| | | 238 | | |
| | | 239 | | // Construct Output Descriptor |
| | 1 | 240 | | const size_t final_offset = idesc.offset + added_offset_bytes; |
| | | 241 | | |
| | 1 | 242 | | holoflow::core::TDesc odesc(out_shape, idesc.dtype, idesc.mem_loc, out_strides, final_offset); |
| | | 243 | | |
| | 1 | 244 | | return holoflow::core::InferResult{ |
| | | 245 | | .input_descs = {idesc}, |
| | | 246 | | .output_descs = {odesc}, |
| | | 247 | | .in_place = {{0, 0}}, // Input 0 -> Output 0 |
| | | 248 | | .owned_inputs = {false}, |
| | | 249 | | .owned_outputs = {false}, |
| | | 250 | | .kind = holoflow::core::TaskKind::Sync, |
| | | 251 | | }; |
| | 1 | 252 | | } |
| | | 253 | | |
| | | 254 | | std::unique_ptr<holoflow::core::ISyncTask> |
| | | 255 | | SliceFactory::create(std::span<const holoflow::core::TDesc> input_descs, |
| | | 256 | | const nlohmann::json &jsettings, |
| | 0 | 257 | | const holoflow::core::SyncCreateCtx &ctx) const { |
| | 0 | 258 | | (void)infer(input_descs, jsettings); |
| | | 259 | | (void)ctx; |
| | 0 | 260 | | return std::make_unique<Slice>(); |
| | 0 | 261 | | } |
| | | 262 | | |
| | | 263 | | std::unique_ptr<holoflow::core::ISyncTask> |
| | | 264 | | SliceFactory::update(std::unique_ptr<holoflow::core::ISyncTask> old_task, |
| | | 265 | | std::span<const holoflow::core::TDesc> input_descs, |
| | | 266 | | const nlohmann::json &jsettings, |
| | 0 | 267 | | const holoflow::core::SyncCreateCtx &ctx) const { |
| | | 268 | | (void)ctx; |
| | 0 | 269 | | (void)infer(input_descs, jsettings); |
| | | 270 | | |
| | 0 | 271 | | auto *old_slice = dynamic_cast<Slice *>(old_task.get()); |
| | 0 | 272 | | if (old_slice == nullptr || input_descs.size() != 1) { |
| | 0 | 273 | | return create(input_descs, jsettings, ctx); |
| | | 274 | | } |
| | | 275 | | |
| | 0 | 276 | | return old_task; |
| | 0 | 277 | | } |
| | | 278 | | |
| | | 279 | | } // namespace holonp |