1#![allow(dead_code)]
58#![allow(non_camel_case_types)]
59
60use crate::c_api::*;
61use parking_lot::Mutex;
62use std::os::raw::{c_char, c_float, c_void};
63use std::ptr;
64use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
65use std::sync::Arc;
66
67#[derive(Clone, Copy)]
73struct TensorAddr(usize);
74unsafe impl Send for TensorAddr {}
75
76impl TensorAddr {
77 fn new(ptr: *mut TorshTensor) -> Self {
78 Self(ptr as usize)
79 }
80 unsafe fn as_ptr(&self) -> *mut TorshTensor {
81 self.0 as *mut TorshTensor
82 }
83}
84
85#[derive(Clone, Copy)]
87struct ModuleAddr(usize);
88unsafe impl Send for ModuleAddr {}
89
90impl ModuleAddr {
91 fn new(ptr: *mut TorshModule) -> Self {
92 Self(ptr as usize)
93 }
94 unsafe fn as_ptr(&self) -> *mut TorshModule {
95 self.0 as *mut TorshModule
96 }
97}
98
99#[derive(Clone, Copy)]
101struct JobjectAddr(usize);
102unsafe impl Send for JobjectAddr {}
103
104impl JobjectAddr {
105 fn new(ptr: jobject) -> Self {
106 Self(ptr as usize)
107 }
108 unsafe fn as_ptr(&self) -> jobject {
109 self.0 as jobject
110 }
111}
112
113#[repr(C)]
115pub struct _JNIEnv {
116 _private: [u8; 0],
117}
118pub type JNIEnv = *mut _JNIEnv;
119
120#[repr(C)]
121pub struct _JavaVM {
122 _private: [u8; 0],
123}
124pub type JavaVM = *mut _JavaVM;
125
126#[repr(C)]
127pub struct _jobject {
128 _private: [u8; 0],
129}
130pub type jobject = *mut _jobject;
131pub type jclass = *mut _jobject;
132pub type jthrowable = *mut _jobject;
133pub type jstring = *mut _jobject;
134
135pub type jlong = i64;
136pub type jint = i32;
137pub type jfloat = f32;
138pub type jdouble = f64;
139pub type jboolean = u8;
140pub type jbyte = i8;
141pub type jchar = u16;
142pub type jshort = i16;
143pub type jsize = jint;
144
145#[repr(C)]
147pub struct _jarray {
148 _private: [u8; 0],
149}
150pub type jarray = *mut _jarray;
151pub type jfloatArray = jarray;
152pub type jintArray = jarray;
153pub type jbyteArray = jarray;
154pub type jobjectArray = jarray;
155
156#[repr(C)]
158pub struct JNINativeInterface {
159 reserved0: *mut c_void,
160 reserved1: *mut c_void,
161 reserved2: *mut c_void,
162 reserved3: *mut c_void,
163
164 get_version: unsafe extern "system" fn(env: JNIEnv) -> jint,
165 }
168
169#[allow(unused_macros)]
171macro_rules! jni_call {
172 ($env:expr, $func:ident $(, $arg:expr)*) => {{
173 }};
177}
178
179#[repr(C)]
185pub struct KotlinCoroutine {
186 id: u64,
187 completed: AtomicBool,
188 result: Mutex<Option<TensorAddr>>,
189 error: Mutex<Option<String>>,
190 continuation: Mutex<Option<JobjectAddr>>,
192}
193
194impl KotlinCoroutine {
195 fn new(id: u64) -> Self {
196 Self {
197 id,
198 completed: AtomicBool::new(false),
199 result: Mutex::new(None),
200 error: Mutex::new(None),
201 continuation: Mutex::new(None),
202 }
203 }
204
205 fn complete_with_result(&self, result: *mut TorshTensor) {
206 *self.result.lock() = Some(TensorAddr::new(result));
207 self.completed.store(true, Ordering::Release);
208 self.resume_continuation();
209 }
210
211 fn complete_with_error(&self, error: String) {
212 *self.error.lock() = Some(error);
213 self.completed.store(true, Ordering::Release);
214 self.resume_continuation();
215 }
216
217 fn resume_continuation(&self) {
218 let continuation = self.continuation.lock();
219 if let Some(cont_addr) = *continuation {
220 let _ = cont_addr;
224 }
225 }
226}
227
228static NEXT_COROUTINE_ID: AtomicU64 = AtomicU64::new(1);
229
230#[no_mangle]
240pub unsafe extern "C" fn Java_com_coolJapan_torsh_Tensor_nativeMatmulAsync(
241 _env: JNIEnv,
242 _class: jclass,
243 a_handle: jlong,
244 b_handle: jlong,
245 continuation: jobject,
246) -> jlong {
247 let id = NEXT_COROUTINE_ID.fetch_add(1, Ordering::Relaxed);
248 let coro = Arc::new(KotlinCoroutine::new(id));
249 *coro.continuation.lock() = Some(JobjectAddr::new(continuation));
250
251 let coro_clone = Arc::clone(&coro);
252 let a_addr = TensorAddr::new(a_handle as *mut TorshTensor);
253 let b_addr = TensorAddr::new(b_handle as *mut TorshTensor);
254
255 std::thread::spawn(move || unsafe {
257 let a = a_addr.as_ptr();
258 let b = b_addr.as_ptr();
259 let result = torsh_tensor_new(ptr::null(), ptr::null(), 0, TorshDType::F32);
260
261 if torsh_tensor_matmul(a, b, result) == TorshError::Success {
262 coro_clone.complete_with_result(result);
263 } else {
264 coro_clone.complete_with_error("Matrix multiplication failed".to_string());
265 }
266 });
267
268 Arc::into_raw(coro) as jlong
269}
270
271#[no_mangle]
273pub unsafe extern "C" fn Java_com_coolJapan_torsh_Model_nativeTrainAsync(
274 _env: JNIEnv,
275 _class: jclass,
276 model_handle: jlong,
277 data_handle: jlong,
278 labels_handle: jlong,
279 epochs: jint,
280 continuation: jobject,
281) -> jlong {
282 let id = NEXT_COROUTINE_ID.fetch_add(1, Ordering::Relaxed);
283 let coro = Arc::new(KotlinCoroutine::new(id));
284 *coro.continuation.lock() = Some(JobjectAddr::new(continuation));
285
286 let coro_clone = Arc::clone(&coro);
287 let model_addr = ModuleAddr::new(model_handle as *mut TorshModule);
288 let data_addr = TensorAddr::new(data_handle as *mut TorshTensor);
289 let labels_addr = TensorAddr::new(labels_handle as *mut TorshTensor);
290
291 std::thread::spawn(move || unsafe {
292 let model = model_addr.as_ptr();
293 let data = data_addr.as_ptr();
294 let labels = labels_addr.as_ptr();
295
296 for epoch in 0..epochs {
297 let output = torsh_tensor_new(ptr::null(), ptr::null(), 0, TorshDType::F32);
299 if torsh_linear_forward(model, data, output) != TorshError::Success {
300 coro_clone.complete_with_error(format!("Training failed at epoch {}", epoch));
301 return;
302 }
303
304 let _ = torsh_tensor_sub(output, labels, output);
306 torsh_tensor_free(output);
307 }
308
309 coro_clone.complete_with_result(model as *mut TorshTensor);
310 });
311
312 Arc::into_raw(coro) as jlong
313}
314
315#[repr(C)]
321pub struct KotlinFlow {
322 operation_id: u64,
323 current_epoch: AtomicU64,
324 total_epochs: u64,
325 current_loss: Mutex<f32>,
326 completed: AtomicBool,
327 collector: Mutex<Option<JobjectAddr>>,
329}
330
331impl KotlinFlow {
332 fn new(total_epochs: u64) -> Self {
333 Self {
334 operation_id: NEXT_COROUTINE_ID.fetch_add(1, Ordering::Relaxed),
335 current_epoch: AtomicU64::new(0),
336 total_epochs,
337 current_loss: Mutex::new(f32::INFINITY),
338 completed: AtomicBool::new(false),
339 collector: Mutex::new(None),
340 }
341 }
342
343 fn emit(&self, env: JNIEnv, value: jobject) {
344 let collector = self.collector.lock();
345 if let Some(coll_addr) = *collector {
346 let _ = (env, coll_addr, value);
349 }
350 }
351}
352
353#[repr(C)]
355pub struct TrainingProgress {
356 pub epoch: u64,
357 pub total_epochs: u64,
358 pub loss: f32,
359 pub accuracy: f32,
360 pub completed: jboolean,
361}
362
363#[no_mangle]
373pub unsafe extern "C" fn Java_com_coolJapan_torsh_Model_nativeTrainingFlow(
374 _env: JNIEnv,
375 _class: jclass,
376 model_handle: jlong,
377 data_handle: jlong,
378 labels_handle: jlong,
379 epochs: jint,
380 collector: jobject,
381) -> jlong {
382 let flow = Arc::new(KotlinFlow::new(epochs as u64));
383 *flow.collector.lock() = Some(JobjectAddr::new(collector));
384
385 let flow_clone = Arc::clone(&flow);
386 let model_addr = ModuleAddr::new(model_handle as *mut TorshModule);
387 let data_addr = TensorAddr::new(data_handle as *mut TorshTensor);
388 let labels_addr = TensorAddr::new(labels_handle as *mut TorshTensor);
389
390 std::thread::spawn(move || unsafe {
392 let model = model_addr.as_ptr();
393 let data = data_addr.as_ptr();
394 let labels = labels_addr.as_ptr();
395
396 for epoch in 0..epochs {
397 flow_clone
398 .current_epoch
399 .store(epoch as u64, Ordering::Relaxed);
400
401 let output = torsh_tensor_new(ptr::null(), ptr::null(), 0, TorshDType::F32);
403 if torsh_linear_forward(model, data, output) != TorshError::Success {
404 break;
405 }
406
407 let _ = torsh_tensor_sub(output, labels, output);
409 *flow_clone.current_loss.lock() = 0.5; torsh_tensor_free(output);
415 std::thread::sleep(std::time::Duration::from_millis(100));
416 }
417
418 flow_clone.completed.store(true, Ordering::Release);
419 });
420
421 Arc::into_raw(flow) as jlong
422}
423
424#[no_mangle]
426pub unsafe extern "C" fn Java_com_coolJapan_torsh_Model_nativeGetFlowProgress(
427 _env: JNIEnv,
428 _class: jclass,
429 flow_handle: jlong,
430) -> TrainingProgress {
431 if flow_handle == 0 {
432 return TrainingProgress {
433 epoch: 0,
434 total_epochs: 0,
435 loss: f32::INFINITY,
436 accuracy: 0.0,
437 completed: 0,
438 };
439 }
440
441 let flow = &*(flow_handle as *const KotlinFlow);
442 TrainingProgress {
443 epoch: flow.current_epoch.load(Ordering::Relaxed),
444 total_epochs: flow.total_epochs,
445 loss: *flow.current_loss.lock(),
446 accuracy: 0.0, completed: flow.completed.load(Ordering::Acquire) as jboolean,
448 }
449}
450
451#[repr(C)]
457pub struct NNAPIDevice {
458 device_id: u32,
459 device_name: [c_char; 256],
460 device_type: u32, version: u32,
462}
463
464#[repr(C)]
466pub struct NNAPICompilation {
467 compilation_handle: *mut c_void,
468 input_count: u32,
469 output_count: u32,
470}
471
472#[no_mangle]
481pub unsafe extern "C" fn Java_com_coolJapan_torsh_NNAPI_nativeCreateDevice(
482 _env: JNIEnv,
483 _class: jclass,
484) -> jlong {
485 let device = Box::new(NNAPIDevice {
492 device_id: 0,
493 device_name: [0; 256],
494 device_type: 0, version: 29, });
497
498 Box::into_raw(device) as jlong
499}
500
501#[no_mangle]
503pub unsafe extern "C" fn Java_com_coolJapan_torsh_NNAPI_nativeCompileModel(
504 _env: JNIEnv,
505 _class: jclass,
506 device_handle: jlong,
507 model_handle: jlong,
508) -> jlong {
509 if device_handle == 0 || model_handle == 0 {
510 return 0;
511 }
512
513 let compilation = Box::new(NNAPICompilation {
523 compilation_handle: ptr::null_mut(),
524 input_count: 1,
525 output_count: 1,
526 });
527
528 Box::into_raw(compilation) as jlong
529}
530
531#[no_mangle]
533pub unsafe extern "C" fn Java_com_coolJapan_torsh_NNAPI_nativeExecute(
534 _env: JNIEnv,
535 _class: jclass,
536 compilation_handle: jlong,
537 input_handle: jlong,
538) -> jlong {
539 if compilation_handle == 0 || input_handle == 0 {
540 return 0;
541 }
542
543 let result = torsh_tensor_new(ptr::null(), ptr::null(), 0, TorshDType::F32);
552 result as jlong
553}
554
555#[no_mangle]
557pub unsafe extern "C" fn Java_com_coolJapan_torsh_NNAPI_nativeFreeDevice(
558 _env: JNIEnv,
559 _class: jclass,
560 device_handle: jlong,
561) {
562 if device_handle != 0 {
563 let _ = Box::from_raw(device_handle as *mut NNAPIDevice);
564 }
565}
566
567#[no_mangle]
569pub unsafe extern "C" fn Java_com_coolJapan_torsh_NNAPI_nativeFreeCompilation(
570 _env: JNIEnv,
571 _class: jclass,
572 compilation_handle: jlong,
573) {
574 if compilation_handle != 0 {
575 let _ = Box::from_raw(compilation_handle as *mut NNAPICompilation);
576 }
577}
578
579#[repr(C)]
600pub struct ComposeModelState {
601 is_training: AtomicBool,
602 current_epoch: AtomicU64,
603 total_epochs: u64,
604 current_loss: Mutex<f32>,
605 recompose_callback: Mutex<Option<unsafe extern "C" fn(*mut c_void)>>,
608 callback_context: *mut c_void,
609}
610
611impl ComposeModelState {
612 fn new(total_epochs: u64) -> Self {
613 Self {
614 is_training: AtomicBool::new(false),
615 current_epoch: AtomicU64::new(0),
616 total_epochs,
617 current_loss: Mutex::new(f32::INFINITY),
618 recompose_callback: Mutex::new(None),
619 callback_context: ptr::null_mut(),
620 }
621 }
622
623 fn trigger_recomposition(&self) {
624 let callback = self.recompose_callback.lock();
625 if let Some(cb) = *callback {
626 unsafe {
627 cb(self.callback_context);
628 }
629 }
630 }
631}
632
633#[no_mangle]
635pub unsafe extern "C" fn Java_com_coolJapan_torsh_compose_ModelState_nativeCreate(
636 _env: JNIEnv,
637 _class: jclass,
638 total_epochs: jint,
639) -> jlong {
640 let state = Box::new(ComposeModelState::new(total_epochs as u64));
641 Box::into_raw(state) as jlong
642}
643
644#[no_mangle]
646pub unsafe extern "C" fn Java_com_coolJapan_torsh_compose_ModelState_nativeSetCallback(
647 _env: JNIEnv,
648 _class: jclass,
649 state_handle: jlong,
650 callback: *mut c_void, context: *mut c_void,
652) {
653 if state_handle == 0 {
654 return;
655 }
656
657 let state = &mut *(state_handle as *mut ComposeModelState);
658 *state.recompose_callback.lock() = Some(std::mem::transmute(callback));
659 state.callback_context = context;
660}
661
662#[no_mangle]
664pub unsafe extern "C" fn Java_com_coolJapan_torsh_compose_ModelState_nativeTrain(
665 _env: JNIEnv,
666 _class: jclass,
667 state_handle: jlong,
668 model_handle: jlong,
669 data_handle: jlong,
670 labels_handle: jlong,
671) {
672 if state_handle == 0 || model_handle == 0 || data_handle == 0 || labels_handle == 0 {
673 return;
674 }
675
676 let state = &*(state_handle as *const ComposeModelState);
677 state.is_training.store(true, Ordering::Release);
678 state.trigger_recomposition();
679
680 let state_ptr = state_handle;
681 let model_addr = ModuleAddr::new(model_handle as *mut TorshModule);
682 let data_addr = TensorAddr::new(data_handle as *mut TorshTensor);
683 let labels_addr = TensorAddr::new(labels_handle as *mut TorshTensor);
684
685 std::thread::spawn(move || unsafe {
687 let state = &*(state_ptr as *const ComposeModelState);
688 let model = model_addr.as_ptr();
689 let data = data_addr.as_ptr();
690 let labels = labels_addr.as_ptr();
691
692 for epoch in 0..state.total_epochs {
693 state.current_epoch.store(epoch, Ordering::Relaxed);
694
695 let output = torsh_tensor_new(ptr::null(), ptr::null(), 0, TorshDType::F32);
696 if torsh_linear_forward(model, data, output) != TorshError::Success {
697 break;
698 }
699
700 let _ = torsh_tensor_sub(output, labels, output);
701 *state.current_loss.lock() = 0.5; state.trigger_recomposition();
704
705 torsh_tensor_free(output);
706 std::thread::sleep(std::time::Duration::from_millis(100));
707 }
708
709 state.is_training.store(false, Ordering::Release);
710 state.trigger_recomposition();
711 });
712}
713
714#[no_mangle]
716pub unsafe extern "C" fn Java_com_coolJapan_torsh_compose_ModelState_nativeIsTraining(
717 _env: JNIEnv,
718 _class: jclass,
719 state_handle: jlong,
720) -> jboolean {
721 if state_handle == 0 {
722 return 0;
723 }
724 let state = &*(state_handle as *const ComposeModelState);
725 state.is_training.load(Ordering::Acquire) as jboolean
726}
727
728#[no_mangle]
730pub unsafe extern "C" fn Java_com_coolJapan_torsh_compose_ModelState_nativeGetEpoch(
731 _env: JNIEnv,
732 _class: jclass,
733 state_handle: jlong,
734) -> jlong {
735 if state_handle == 0 {
736 return 0;
737 }
738 let state = &*(state_handle as *const ComposeModelState);
739 state.current_epoch.load(Ordering::Acquire) as jlong
740}
741
742#[no_mangle]
744pub unsafe extern "C" fn Java_com_coolJapan_torsh_compose_ModelState_nativeGetLoss(
745 _env: JNIEnv,
746 _class: jclass,
747 state_handle: jlong,
748) -> jfloat {
749 if state_handle == 0 {
750 return f32::INFINITY;
751 }
752 let state = &*(state_handle as *const ComposeModelState);
753 *state.current_loss.lock()
754}
755
756#[no_mangle]
762pub unsafe extern "C" fn Java_com_coolJapan_torsh_Tensor_nativeFromFloatArray(
763 env: JNIEnv,
764 _class: jclass,
765 data_array: jfloatArray,
766 shape_array: jintArray,
767) -> jlong {
768 if data_array.is_null() || shape_array.is_null() {
769 return 0;
770 }
771
772 let _ = env;
780 let tensor = torsh_tensor_new(ptr::null(), ptr::null(), 0, TorshDType::F32);
781
782 tensor as jlong
787}
788
789#[no_mangle]
791pub unsafe extern "C" fn Java_com_coolJapan_torsh_Tensor_nativeToFloatArray(
792 env: JNIEnv,
793 _class: jclass,
794 tensor_handle: jlong,
795 output_array: jfloatArray,
796) -> jboolean {
797 if tensor_handle == 0 || output_array.is_null() {
798 return 0;
799 }
800
801 let tensor = tensor_handle as *const TorshTensor;
802 let data_ptr = torsh_tensor_data(tensor) as *const c_float;
803
804 if data_ptr.is_null() {
805 return 0;
806 }
807
808 let _ = (env, data_ptr);
815 1
816}
817
818#[no_mangle]
820pub unsafe extern "C" fn Java_com_coolJapan_torsh_Tensor_nativeGetShapeArray(
821 env: JNIEnv,
822 _class: jclass,
823 tensor_handle: jlong,
824) -> jintArray {
825 if tensor_handle == 0 {
826 return ptr::null_mut();
827 }
828
829 let tensor = tensor_handle as *mut TorshTensor;
830 let mut shape = vec![0usize; 16];
831 let mut ndim = 0usize;
832
833 if torsh_tensor_shape(tensor, shape.as_mut_ptr(), &mut ndim) != TorshError::Success {
834 return ptr::null_mut();
835 }
836
837 let _ = (env, shape, ndim);
847 ptr::null_mut()
848}
849
850#[repr(C)]
856pub struct TFLiteInterpreter {
857 interpreter_ptr: *mut c_void,
858 input_count: u32,
859 output_count: u32,
860}
861
862#[no_mangle]
864pub unsafe extern "C" fn Java_com_coolJapan_torsh_TFLite_nativeCreateInterpreter(
865 env: JNIEnv,
866 _class: jclass,
867 model_path: jstring,
868) -> jlong {
869 if model_path.is_null() {
870 return 0;
871 }
872
873 let _ = env;
880 let interpreter = Box::new(TFLiteInterpreter {
881 interpreter_ptr: ptr::null_mut(),
882 input_count: 1,
883 output_count: 1,
884 });
885
886 Box::into_raw(interpreter) as jlong
887}
888
889#[no_mangle]
891pub unsafe extern "C" fn Java_com_coolJapan_torsh_TFLite_nativeInvoke(
892 _env: JNIEnv,
893 _class: jclass,
894 interpreter_handle: jlong,
895 input_handle: jlong,
896) -> jlong {
897 if interpreter_handle == 0 || input_handle == 0 {
898 return 0;
899 }
900
901 let result = torsh_tensor_new(ptr::null(), ptr::null(), 0, TorshDType::F32);
903 result as jlong
904}
905
906#[repr(C)]
912pub enum LifecycleEvent {
913 OnCreate,
914 OnStart,
915 OnResume,
916 OnPause,
917 OnStop,
918 OnDestroy,
919}
920
921#[repr(C)]
923pub struct LifecycleTrainingManager {
924 is_active: AtomicBool,
925 should_pause: AtomicBool,
926 model_handle: *mut TorshModule,
927}
928
929#[no_mangle]
931pub unsafe extern "C" fn Java_com_coolJapan_torsh_lifecycle_TrainingManager_nativeCreate(
932 _env: JNIEnv,
933 _class: jclass,
934 model_handle: jlong,
935) -> jlong {
936 let manager = Box::new(LifecycleTrainingManager {
937 is_active: AtomicBool::new(false),
938 should_pause: AtomicBool::new(false),
939 model_handle: model_handle as *mut TorshModule,
940 });
941
942 Box::into_raw(manager) as jlong
943}
944
945#[no_mangle]
947pub unsafe extern "C" fn Java_com_coolJapan_torsh_lifecycle_TrainingManager_nativeOnLifecycleEvent(
948 _env: JNIEnv,
949 _class: jclass,
950 manager_handle: jlong,
951 event: jint,
952) {
953 if manager_handle == 0 {
954 return;
955 }
956
957 let manager = &*(manager_handle as *const LifecycleTrainingManager);
958
959 match event {
960 0 => { }
961 1 => {
962 manager.is_active.store(true, Ordering::Release);
964 }
965 2 => {
966 manager.should_pause.store(false, Ordering::Release);
968 }
969 3 => {
970 manager.should_pause.store(true, Ordering::Release);
972 }
973 4 => {
974 manager.is_active.store(false, Ordering::Release);
976 }
977 5 => {
978 manager.is_active.store(false, Ordering::Release);
980 }
981 _ => {}
982 }
983}
984
985#[cfg(test)]
990mod tests {
991 use super::*;
992
993 #[test]
994 fn test_kotlin_coroutine_creation() {
995 let coro = KotlinCoroutine::new(1);
996 assert!(!coro.completed.load(Ordering::Acquire));
997 }
998
999 #[test]
1000 fn test_kotlin_flow_creation() {
1001 let flow = KotlinFlow::new(10);
1002 assert_eq!(flow.total_epochs, 10);
1003 assert_eq!(flow.current_epoch.load(Ordering::Relaxed), 0);
1004 }
1005
1006 #[test]
1007 fn test_nnapi_device_structure() {
1008 let device = NNAPIDevice {
1009 device_id: 0,
1010 device_name: [0; 256],
1011 device_type: 0,
1012 version: 29,
1013 };
1014 assert_eq!(device.version, 29);
1015 }
1016
1017 #[test]
1018 fn test_compose_state_creation() {
1019 let state = ComposeModelState::new(100);
1020 assert_eq!(state.total_epochs, 100);
1021 assert!(!state.is_training.load(Ordering::Acquire));
1022 }
1023
1024 #[test]
1025 fn test_lifecycle_manager() {
1026 let manager = LifecycleTrainingManager {
1027 is_active: AtomicBool::new(false),
1028 should_pause: AtomicBool::new(false),
1029 model_handle: ptr::null_mut(),
1030 };
1031 assert!(!manager.is_active.load(Ordering::Acquire));
1032 }
1033
1034 #[test]
1035 fn test_training_progress_structure() {
1036 let progress = TrainingProgress {
1037 epoch: 5,
1038 total_epochs: 10,
1039 loss: 0.5,
1040 accuracy: 0.9,
1041 completed: 0,
1042 };
1043 assert_eq!(progress.epoch, 5);
1044 assert_eq!(progress.total_epochs, 10);
1045 }
1046}