diff --git a/middle_layer/allocate/domain_layer/entities/src/allocated_science_target.py b/middle_layer/allocate/domain_layer/entities/src/allocated_science_target.py
index 82a0a4f6cb7304fd695d405d2328218db94d6f3d..a019fff868a53fb4cea3ed61de52175d75f5444e 100644
--- a/middle_layer/allocate/domain_layer/entities/src/allocated_science_target.py
+++ b/middle_layer/allocate/domain_layer/entities/src/allocated_science_target.py
@@ -172,7 +172,7 @@ class AllocatedScienceTarget(Base):
     )
     hardware_configuration: HardwareConfiguration = relationship(HardwareConfiguration)
 
-    requested_time: Quantity["time"] = Column("requested_time", QuantitySeconds, nullable=False)
+    requested_time: Quantity["time"] = Column("requested_time", QuantitySeconds, nullable=False, default=0 * u.s)
 
     calibration_strategy: CalibrationStrategy = Column(
         "calibration_strategy", sa.Enum(CalibrationStrategy), nullable=False
diff --git a/middle_layer/allocate/domain_layer/services/src/create_allocated_science_target_list.py b/middle_layer/allocate/domain_layer/services/src/create_allocated_science_target_list.py
index c91970aab806cca10f88e2b798a8779761ecef41..2c1efedbcad94529795d8a466bc460186d8024bf 100644
--- a/middle_layer/allocate/domain_layer/services/src/create_allocated_science_target_list.py
+++ b/middle_layer/allocate/domain_layer/services/src/create_allocated_science_target_list.py
@@ -14,6 +14,11 @@
 #
 # You should have received a copy of the GNU General Public License
 # along with TTAT.  If not, see <https://www.gnu.org/licenses/>.
+from functools import cache
+
+import astropy.units as u
+from astropy.units import Quantity
+
 from allocate.domain_layer.entities.src.allocated_science_target import (
     AllocatedScienceTarget,
     CalibrationStrategy,
@@ -48,62 +53,115 @@ def create_allocated_science_targets(
     *, allocation_disposition: AllocationDisposition, allocation_request: AllocationRequest, repo: ORMRepository
 ) -> list[AllocatedScienceTarget]:
 
+    # TODO: once the unmodified code path exists, remove this line of code
+    return create_modified_allocated_science_targets(allocation_disposition, allocation_request)
+
     # if any of the Observation Specifications are modified, we can build the ASTL
     if any(os.modified for os in allocation_request.observation_specifications):
-        # these are basically caches
-        sources = {}
-        hardware_configs = {}
-
-        # create the ASTs
-        for os in allocation_request.observation_specifications:
-            os: ObservationSpecification
-
-            for subscan in os.science_target_subscans:
-                # the plan here is to find or create a source, and then find or create a hardware configuration
-                # and then stitch them together into an AST instance
-
-                # first the source
-                source = None
-                source_key = (subscan.target_name, subscan.right_ascension, subscan.declination)
-                if source_key in sources:
-                    source = sources[source_key]
-                else:
-                    # TODO: where do we find the pointing pattern?
-                    source = Source(
-                        name=subscan.target_name,
-                        right_ascension=subscan.right_ascension,
-                        declination=subscan.declination,
-                        pointing_pattern=POINTING_PATTERNS[os.facility.facility_name],
-                    )
-                    sources[source_key] = source
-
-                # same song with hardware configurations
-                hardware_configuration = None
-                hardware_config_key = (subscan.frontend, subscan.backend)
-                if hardware_config_key in hardware_configs:
-                    hardware_configuration = hardware_configs[hardware_config_key]
-                else:
-                    hardware_configuration = HardwareConfiguration(frontend=subscan.frontend, backend=subscan.backend)
-                    hardware_configuration.facility_configurations = os.facility_configurations
-                    hardware_configs[hardware_config_key] = hardware_configuration
-
-                # now we can create the AST
-                # TODO: where do we find the scheduling and calibration strategies?
-                ast = AllocatedScienceTarget(
-                    allocation_disposition=allocation_disposition,
-                    hardware_configuration=hardware_configuration,
-                    source=source,
-                    scheduling_strategy=SCHEDULING_STRATEGIES[os.facility.facility_name],
-                    calibration_strategy=CALIBRATION_STRATEGIES[os.facility.facility_name],
-                )
-
-                allocation_disposition.allocated_science_targets.append(ast)
-
-        # return the list on the AD for no particular reason
-        return allocation_disposition.allocated_science_targets
+        return create_modified_allocated_science_targets(allocation_disposition, allocation_request)
     else:
-        # in theory, we should at this point copy the STL instead
-        # unfortunately, as of Sprint 90, the STL is still garbage
-        raise NotImplementedError(
-            "unmodified obs-specs can not be used as we do not currently have a valid science target list to copy"
-        )
+        return create_unmodified_allocated_science_targets(allocation_disposition, allocation_request)
+
+
+def create_unmodified_allocated_science_targets(
+    allocation_disposition: AllocationDisposition, allocation_request: AllocationRequest
+) -> list[AllocatedScienceTarget]:
+    """
+    Generate the ASTs from the science targets
+
+    :param allocation_disposition:
+    :param allocation_request:
+    :return:
+    """
+    # in theory, we should at this point copy the STL instead
+    # unfortunately, as of Sprint 90, the STL is still garbage
+    raise NotImplementedError(
+        "unmodified obs-specs can not be used as we do not currently have a valid science target list to copy"
+    )
+
+
+def create_modified_allocated_science_targets(
+    allocation_disposition, allocation_request
+) -> list[AllocatedScienceTarget]:
+    """
+    Generate the ASTs from the observation specs and subscans.
+
+    :param allocation_disposition:
+    :param allocation_request:
+    :return:
+    """
+    # create the ASTs
+    all_asts = set()
+
+    # look through all the observation specifications
+    for os in allocation_request.observation_specifications:
+        os: ObservationSpecification
+
+        # The following three functions exist to support caching the sources, hardware configurations and
+        # allocated science targets. The caching is desired so that all of the ASTs we make will share the
+        # same source or hardware configuration _if_ the source's fields or hardware configuration's fields
+        # are the same.
+        #
+        # They are defined here with the cache annotation because if they were defined above this level, the cache
+        # would persist across observation specs, which is undesired. All of these entities should be unique _within_
+        # an OS but not _across_ OSes.
+
+        # Generate (or look up) a source
+        @cache
+        def source_for(target_name: str, right_ascension: Quantity["angle"], declination: Quantity["angle"]):
+            # TODO: Where do we find the pointing patterns?
+            return Source(
+                name=target_name,
+                right_ascension=right_ascension,
+                declination=declination,
+                pointing_pattern=POINTING_PATTERNS[os.facility.facility_name],
+            )
+
+        # Generate (or look up) a hardware configuration
+        @cache
+        def hardware_config_for(frontend: str, backend: str):
+            hwc = HardwareConfiguration(frontend=frontend, backend=backend)
+            hwc.facility_configurations = os.facility_configurations
+            return hwc
+
+        # Generate (or look up) an allocated science target
+        @cache
+        def ast_for(source: Source, hardware_config: HardwareConfiguration):
+            # TODO: Where do we find the scheduling and calibration strategies?
+            return AllocatedScienceTarget(
+                allocation_disposition=allocation_disposition,
+                hardware_configuration=hardware_config,
+                source=source,
+                scheduling_strategy=SCHEDULING_STRATEGIES[os.facility.facility_name],
+                calibration_strategy=CALIBRATION_STRATEGIES[os.facility.facility_name],
+                requested_time=0 * u.s,
+            )
+
+        # The body of the loop is here, where we examine all of the *science* target subscans
+        # With each subscan, we obtain an AST using the above functions, and then aggregate onto it
+        # the acquisition time from the supporting subscan. After all the subscans have been accounted for,
+        # the requested time on the AST should be the sum of all the acquisition times for the relevant
+        # subscans.
+        #
+        # Because these are aggregated at the OS level, there should not be duplicate ASTs under an OS, but
+        # there may be duplicate ASTs between multiple OSes.
+        for subscan in os.science_target_subscans:
+            # the plan here is to find or create a source, and then find or create a hardware configuration
+            # and then stitch them together into an AST instance
+
+            # obtain the (cached) source and hardware configurations
+            src = source_for(subscan.target_name, subscan.right_ascension, subscan.declination)
+            hardware_configuration = hardware_config_for(subscan.frontend, subscan.backend)
+
+            # now we can create the AST and keep it for later
+            ast = ast_for(src, hardware_configuration)
+            all_asts.add(ast)
+
+            # add the acquisition time from this subscan to this ast
+            ast.requested_time += subscan.acquisition_time if subscan.acquisition_time is None else 0 * u.s
+
+    # add all the ASTs to the allocated science targets list
+    allocation_disposition.allocated_science_targets.extend(all_asts)
+
+    # return the list on the AD for no particular reason
+    return allocation_disposition.allocated_science_targets
diff --git a/middle_layer/allocate/domain_layer/services/test/test_create_allocated_science_target_list.py b/middle_layer/allocate/domain_layer/services/test/test_create_allocated_science_target_list.py
index c2c4885632787817268f554a2748f58953348a5c..f2dc288a1d9f174bb89c8edcf39811d7f2905d94 100644
--- a/middle_layer/allocate/domain_layer/services/test/test_create_allocated_science_target_list.py
+++ b/middle_layer/allocate/domain_layer/services/test/test_create_allocated_science_target_list.py
@@ -25,6 +25,9 @@ from allocate.domain_layer.services.src.create_allocation_version_service import
 from common.application_layer.orm_repositories.src.orm_repository import ORMRepository
 from propose.domain_layer.entities.src.observation_specification import ObservationSpecification, ScienceTarget
 from propose.domain_layer.entities.src.proposal import AllocationRequest, ProposalCopy
+from propose.domain_layer.services.src.observation_specification_generator_service import (
+    generate_observation_specifications,
+)
 from solicit.application_layer.services.src.close_solicitation_service import close_solicitation
 from solicit.domain_layer.entities.src.solicitation import ProposalProcess
 from testdata.application_layer.services.src.context import Context
@@ -36,8 +39,8 @@ def create_context_for_test(repo: ORMRepository, requests_mock_notify_good: Mock
     global solicitation_id
     context = Context(repo.session)
     pp: ProposalProcess = repo.proposal_process_repo.by_name("Panel Proposal Review")
-    context.make_solicitation("Test", proposal_process=pp)
-    context.make_proposals(1, do_submit=True)
+    context.make_solicitation("Test", proposal_process=pp, random_seed=20)
+    proposals = context.make_proposals(1, do_submit=True)
     close_solicitation(context.solicitation, repo)
     context.run_all_ppr_process_steps()
     av = create_allocation_version(
@@ -108,7 +111,6 @@ def test_unmodified_generates_astl_equal_to_stl(context):
         assert derived_from(science_target, allocated_science_target)
 
 
-@pytest.mark.skip("Random failures ~30% of the time")
 def test_modified_generates_astl_from_scratch(context):
     ar = modify_n_allocation_dispositions(1, context)
     ad: AllocationDisposition = ar.allocation_dispositions[0]
@@ -128,16 +130,62 @@ def test_modified_generates_astl_from_scratch(context):
     for obspec in ad.allocation_request.observation_specifications:
         obspec: ObservationSpecification
 
-        for hw_conf in obspec.hardware_configs:
-            # somewhere in there, we have this hardware configuration
-            assert (hw_conf.frontend, hw_conf.backend) in hw_configs
+        for subscan in obspec.science_target_subscans:
+            assert (subscan.frontend, subscan.backend) in hw_configs
 
         for sts in obspec.science_target_subscans:
             # somewhere in there, we have these sources
             assert (sts.right_ascension, sts.declination, sts.target_name) in sources
 
 
-@pytest.mark.skip("Nothing to cleanup since other tests are skipped")
+# @pytest.mark.skip("This test is not implemented yet")
+def test_ensure_no_duplicate_asts(context):
+    ar = modify_n_allocation_dispositions(1, context)
+    ad: AllocationDisposition = ar.allocation_dispositions[0]
+
+    # count how many ST subscans we have, should get an ASTL of this length
+    total_subscans = 0
+    for obspec in ar.observation_specifications:
+        total_subscans += len(obspec.science_target_subscans)
+
+    # make identical obspecs, and put one on the ar
+    all_scan_intents = {si.name: si for si in context.repo.scan_intent_repo.list_all()}
+    all_subscan_intents = {ssi.name: ssi for ssi in context.repo.subscan_intent_repo.list_all()}
+    obspecs_copy = generate_observation_specifications(
+        ar, all_subscan_intents["ON_SOURCE"], all_subscan_intents["OFF_SOURCE"], all_scan_intents["OBSERVE_TARGET"]
+    )
+    new_obspec = obspecs_copy.pop()
+    # this awful block ensures SQLAlchemy can comprehend scan intents
+    for j, _ in enumerate(new_obspec.scans):
+        new_obspec.scans[j].scan_intents = [
+            all_scan_intents[scan_intent.name] for scan_intent in new_obspec.scans[j].scan_intents
+        ]
+        subscans = new_obspec.scans[j].subscans
+        for k, _ in enumerate(subscans):
+            subscans[k].subscan_intent = all_subscan_intents[subscans[k].subscan_intent.name]
+    context.repo.session.add(new_obspec)
+    ar.observation_specifications.append(new_obspec)
+
+    # now make sure we have extra subscans in the new set
+    total_subscans_with_duplicate_os = 0
+    for obspec in ar.observation_specifications:
+        total_subscans_with_duplicate_os += len(obspec.science_target_subscans)
+    assert total_subscans_with_duplicate_os > total_subscans
+    # Make the ASTL and ensure it is consistent with length of original set of ST subscans
+    create_allocated_science_targets(
+        allocation_disposition=ad, allocation_request=ad.allocation_request, repo=context.repo
+    )
+    assert len(ad.allocated_science_targets) == total_subscans
+
+
+@pytest.mark.skip("This test is not implemented yet")
+def test_ensure_asts_get_time():
+    # step one is to arrange for an OS that would generate two separate additions of time on the AST
+
+    # step two is to look at the AST and see if we got that sum
+    raise NotImplementedError
+
+
 def test_cleanup(context: Context, repo: ORMRepository):
     context.cleanup()
     repo.session.commit()