diff --git a/pypact/input/inputdata.py b/pypact/input/inputdata.py index a2e43f7..9f0fa40 100644 --- a/pypact/input/inputdata.py +++ b/pypact/input/inputdata.py @@ -427,8 +427,18 @@ def _deserialize(self, f): """ self.reset() - lines = f.readlines() + lines = iter(f.readlines()) # Convert lines to an iterator in_mass_section = False + irradiation_active = True # Flag to track if irradiation schedule is active + flux_set = False # Flag to ensure FLUX is set to a non-zero value before the first irradiation step + + time_unit_to_seconds = { + "SECS": 1, + "MINS": 60, + "HOURS": 3600, + "DAYS": 86400, + "YEARS": 31536000 + } for line in lines: line = line.strip() @@ -451,6 +461,9 @@ def _deserialize(self, f): if num_elements == 0: in_mass_section = False else: + if line.startswith(COMMENT_START) and line.endswith(COMMENT_END): + # Ignore comments + continue raise PypactInvalidOptionException("Invalid element line format in MASS section.") elif line.startswith("DENSITY"): @@ -458,3 +471,64 @@ def _deserialize(self, f): if len(parts) != 2: raise PypactInvalidOptionException("Invalid DENSITY line format.") self.setDensity(float(parts[1])) + + elif line.startswith("FLUX") and irradiation_active: + # Parse the FLUX line + parts = line.split() + if len(parts) != 2: + raise PypactInvalidOptionException("Invalid FLUX line format.") + fluxAmp = float(parts[1]) + + # Ensure FLUX is set to a non-zero value before the first irradiation step + if not flux_set and fluxAmp <= 0.0: + raise PypactInvalidOptionException("FLUX must be set to a non-zero positive value before the first irradiation step.") + flux_set = True + + # Store the current flux amplitude - we'll use it when we find a TIME command + current_flux_amp = fluxAmp + + # If FLUX is 0.0, we're entering the cooling phase + if fluxAmp == 0.0: + self.addIrradiation(0.0, fluxAmp) + continue + + # Handle TIME entries - can appear after any number of intermediate commands following FLUX + elif line.startswith("TIME") and flux_set: + time_parts = line.split() + + # Ensure we have at least the TIME keyword and a value + if len(time_parts) < 2: + raise PypactInvalidOptionException( + "Invalid TIME line format: missing time value." + ) + + # Get the time value + time_value = float(time_parts[1]) + timeInSecs = time_value # Default unit is seconds + + # Handle different TIME formats + if len(time_parts) >= 3: + # Check if the third part is a time unit + if time_parts[2] in time_unit_to_seconds: + # Format: TIME 10.0 SECS or TIME 0.1 YEARS ATOMS + timeInSecs = time_value * time_unit_to_seconds[time_parts[2]] + # Otherwise, the third part is likely "ATOMS" or something else + # We keep the default assumption of seconds + + # Add the time to the appropriate schedule based on whether we're in irradiation or cooling phase + if irradiation_active: + # Still in irradiation phase + self.addIrradiation(timeInSecs, current_flux_amp) + else: + # In cooling phase after ZERO + self.addCooling(timeInSecs) + + elif line.startswith("ZERO"): + # Ensure FLUX is set to zero before using ZERO + if not flux_set: + raise PypactInvalidOptionException( + "FLUX must be set before using the ZERO keyword." + ) + # Stop processing irradiation schedule + irradiation_active = False + current_flux_amp = 0.0 diff --git a/reference/test.i b/reference/test.i index 48d4c39..471d4cb 100644 --- a/reference/test.i +++ b/reference/test.i @@ -42,6 +42,8 @@ ATOMS << irradiation schedule >> FLUX 1100000000000000.0 TIME 300.0 SECS +FLUX 42.0 +TIME 200.0 SECS ATOMS << end of irradiation >> FLUX 0.0 diff --git a/reference/test2.i b/reference/test2.i new file mode 100644 index 0000000..25b472b --- /dev/null +++ b/reference/test2.i @@ -0,0 +1,52 @@ +<< -----set initial switches and get nuclear data----- >> +CLOBBER +JSON +SPEK +GETXS 1 709 +GETDECAY 1 +FISPACT +* FNS 5 Minutes Inconel-600 +DENSITY 8.42 +MASS 1.0E-3 4 +NI 75.82 +MN 0.39 +FE 7.82 +CR 15.97 +MIND 1E3 +GRAPH 1 2 1 3 +UNCERTAINTY 2 +HALF +HAZARDS +TAB1 1 +<< -----irradiation phase----- >> +FLUX 1.116E+10 +ATOMS +TIME 5.0 MINS +ATOMS +<< -----cooling phase----- >> +FLUX 0. +ZERO +TIME 36 ATOMS +TIME 15 ATOMS +TIME 16 ATOMS +TIME 15 ATOMS +TIME 15 ATOMS +TIME 26 ATOMS +TIME 33 ATOMS +TIME 36 ATOMS +TIME 53 ATOMS +TIME 66 ATOMS +TIME 66 ATOMS +TIME 97 ATOMS +TIME 127 ATOMS +TIME 126 ATOMS +TIME 187 ATOMS +TIME 246 ATOMS +TIME 244 ATOMS +TIME 246 ATOMS +TIME 428 ATOMS +TIME 606 ATOMS +TIME 607 ATOMS +END +* END +/* \ No newline at end of file diff --git a/reference/test3.i b/reference/test3.i new file mode 100644 index 0000000..036a7a7 --- /dev/null +++ b/reference/test3.i @@ -0,0 +1,16 @@ +MONITOR 1 +CLOBBER +JSON +SPEK +GETXS 1 709 +GETDECAY 1 +FISPACT +* irradiation 1 +MASS 1.0 1 +W 100.0 +TAB1 21 +FLUX 1.1e14 +ATOMS +TIME 5 MINS ATOMS +END +* end \ No newline at end of file diff --git a/reference/test4.i b/reference/test4.i new file mode 100644 index 0000000..9ca9a86 --- /dev/null +++ b/reference/test4.i @@ -0,0 +1,82 @@ +MONITOR 1 +<< Overwrite existing inventory.log and inventory.out files >> +CLOBBER +<< Enable JSON file format output for inventory data >> +JSON +<< Read ARRAYX and COLLAPX files >> +GETXS 1 709 +GETDECAY 1 +<< Read gamma bounds from file >> +READGG +<< End of control >> +FISPACT +* FNS 5 Minutes Inconel-600 +<< Material definition - start of initialisation phase >> +<< Density is in units of g/cm3 >> +DENSITY 8.42 +<< Elemental definition of material >> +<< total mass = 1g, with 4 elements>> +MASS 1.0E-3 4 +<< Nickel at 75.82%>> +NI 75.82 +<< Manganese at 0.39%>> +MN 0.39 +<< Iron at 7.82%>> +FE 7.82 +<< Chromium at 15.97%>> +CR 15.97 +<< Set the minimum number of atoms to track - 1000 atom threshold>> +<< 1e5 atoms is the default >> +MIND 1E3 +<< Produce some graph files for GNU plot for post processing >> +<< from left to right: 1 graph, 2= .gra and .plt for gnuplot, 1=use uncertainties, 3=total heat output>> +GRAPH 1 2 1 3 +<< Output estimates of both uncertainty and pathway analysis >> +UNCERTAINTY 2 +<< Output half lives to output inventory information >> +HALF +<< Output ingestion and inhalation doses to output >> +HAZARDS +<< Signify start of inventory phase >> +<< -----irradiation phase----- >> +<< Flux amplitude in /cm2 /s >> +FLUX 1.116E+10 +<< ATOMS= tells F-II to solve rate equations and dump output to file >> +<< No time is given so it will use 0.0 (initial) >> +ATOMS +<< Solve and output at 5 minutes of irradiation>> +TIME 5.0 MINS +ATOMS +<< -----cooling phase----- >> +<< Set flux to 0 to tell F-II that no irradiation is occuring and it is decay only >> +FLUX 0. +<< Whilst it is possible to irradiate again FLUX >0, ZERO tells F-II that no more irradiation can occur >> +<< From here on only cooling can happen - this keyword is important for pathways analysis>> +ZERO +<< Cooling time - note use of ATOMS to indicate solving and output >> +<< we could also use STEP or SPECTRUM to reduce output>> +<< Default time unit is seconds, these are all in seconds >> +TIME 36 ATOMS +TIME 15 ATOMS +TIME 16 ATOMS +TIME 15 ATOMS +TIME 15 ATOMS +TIME 26 ATOMS +TIME 33 ATOMS +TIME 36 ATOMS +TIME 53 ATOMS +TIME 66 ATOMS +TIME 66 ATOMS +TIME 97 ATOMS +TIME 127 ATOMS +TIME 126 ATOMS +TIME 187 ATOMS +TIME 246 ATOMS +TIME 244 ATOMS +TIME 246 ATOMS +TIME 428 ATOMS +TIME 606 ATOMS +TIME 607 ATOMS +<< End of file >> +END +* END diff --git a/reference/test5.i b/reference/test5.i new file mode 100644 index 0000000..75c166a --- /dev/null +++ b/reference/test5.i @@ -0,0 +1,28 @@ +MONITOR 1 +CLOBBER +JSON +SPEK +NOERROR +PROJECTILE 3 +GETXS 1 162 +GETDECAY 1 +FISPACT +* proton irradiation +DENSITY 1.0 +MASS 1.0 1 +Y 100. +FLUX 1e20 +ATOMS +TIME 1 MINS ATOMS +TIME 1 MINS ATOMS +TIME 1 MINS ATOMS +FLUX 0.0 +TIME 1 DAYS ATOMS +TIME 1 DAYS ATOMS +TIME 1 DAYS ATOMS +TIME 1 DAYS ATOMS +TIME 1 DAYS ATOMS +TIME 1 DAYS ATOMS +TIME 1 DAYS ATOMS +END +* end \ No newline at end of file diff --git a/tests/input/inputfiletest.py b/tests/input/inputfiletest.py index 8c7712e..b318b84 100644 --- a/tests/input/inputfiletest.py +++ b/tests/input/inputfiletest.py @@ -34,3 +34,116 @@ def test_reading_in_density(self): pp.from_file(ff, 'reference/test.i') assert ff._density == 19.5 + + def test_reading_irradschedule(self): + test_cases = [ + ( + "reference/test.i", + [ + (300.0, 1.1e15), + (200.0, 42.0), + (0.0, 0.0), + ], + ), + ("reference/test2.i", [(300.0, 1.116e10), (0.0, 0.0)]), + ( + "reference/test3.i", + [ + (300.0, 1.1e14), + ], + ), + ("reference/test4.i", [(300.0, 1.116e10), (0.0, 0.0)]), + # test case has no ZERO keyword + ( + "reference/test5.i", + [ + (60.0, 1e20), + (60.0, 1e20), + (60.0, 1e20), + (0, 0), + (86400, 0), + (86400, 0), + (86400, 0), + (86400, 0), + (86400, 0), + (86400, 0), + (86400, 0), + ], + ), + ] + for input_file, expected_schedule in test_cases: + with self.subTest(input_file=input_file): + ff = pp.InputData() + pp.from_file(ff, input_file) + + self.assertEqual(len(ff._irradschedule), len(expected_schedule)) + for i, (time, flux) in enumerate(expected_schedule): + self.assertEqual(ff._irradschedule[i][0], time) + self.assertEqual(ff._irradschedule[i][1], flux) + + def test_reading_coolingschedule(self): + test_cases = [ + ("reference/test.i", [10.0, 100.0, 1000.0, 10000.0, 100000.0]), + ( + "reference/test2.i", + [ + 36, + 15, + 16, + 15, + 15, + 26, + 33, + 36, + 53, + 66, + 66, + 97, + 127, + 126, + 187, + 246, + 244, + 246, + 428, + 606, + 607, + ], + ), + ("reference/test3.i", []), + ( + "reference/test4.i", + [ + 36, + 15, + 16, + 15, + 15, + 26, + 33, + 36, + 53, + 66, + 66, + 97, + 127, + 126, + 187, + 246, + 244, + 246, + 428, + 606, + 607, + ], + ), + # test case has no ZERO keyword + ("reference/test5.i", []), + ] + + for input_file, expected_schedule in test_cases: + with self.subTest(input_file=input_file): + ff = pp.InputData() + pp.from_file(ff, input_file) + + self.assertEqual(ff._coolingschedule, expected_schedule)