def _try_apply_transform(
        self,
        last_op: Operation,
        _child_op: Operation,
        block: Block,
        visited_ops: set,
        op_config: OpLinearQuantizerConfig,
    ) -> bool:
        """
        last_op: last op of a valid pattern.
                 E.g. in `conv` -> `relu`, last_op is `relu`; in `conv`, last_op is `conv`.
        _child_op: the child op of the last_op.
        block: current block.
        visited_ops: a dict

        Pattern:
            Given:
                           |-> child_op_1
                last_op -> |-> child_op_2
                           |-> ...
            Result:
                                                     |-> child_op_1
                last_op -> quantize -> dequantize -> |-> child_op_2
                                                     |-> ...
        """
        if _child_op is None:
            return False

        new_last_op = getattr(mb, last_op.op_type)
        kargs = {}
        for k, v in last_op.inputs.items():
            kargs[k] = v
        kargs["name"] = last_op.name
        kargs["before_op"] = last_op
        new_last_op = new_last_op(**kargs)

        var_name = last_op.outputs[0].name
        val = optimize_utils.get_min_and_max_values(self._activation_stats, var_name)

        # Numerically the scale and zero point won't change if the input array only have two elements:
        # the min and max values of the input array. That's the trick to re-use optimize_utils.quantize_weight util.
        _dtype = np.int8 if op_config.signed else np.uint8
        _, _scale, _zero_point = optimize_utils.quantize_weight(
            val,
            axes=0,
            nbits=8,
            signed=op_config.signed,
            quantization_mode=op_config.mode,
            dtype=_dtype,
        )
        if _zero_point is None:
            _zero_point = np.int32(0)
        new_quantize_op = mb.quantize(
            input=new_last_op,
            scale=_scale,
            zero_point=_zero_point.astype(_dtype),
            output_dtype="int8" if op_config.signed else "uint8",
            before_op=last_op,
        )
        new_dequantize_op = mb.dequantize(
            input=new_quantize_op,
            scale=_scale,
            zero_point=_zero_point.astype(_dtype),
            before_op=last_op,
        )
        ops_to_remove = [last_op]

        last_op_var_name = last_op.outputs[0].name
        # Replace output var of last_op with output of new_dequantize_op.
        if last_op.enclosing_block.try_replace_uses_of_var_after_op(
            anchor_op=last_op,
            end_op=last_op,
            old_var=last_op.outputs[0],
            new_var=new_dequantize_op,
        ):
            block.remove_ops(ops_to_remove)
            # The name of new quantize/dequantize may change.
            # Add the new ones to the visited list to avoid revisiting.
            visited_ops.add(new_dequantize_op.op)
            visited_ops.add(new_quantize_op.op)
            new_dequantize_var_name = new_dequantize_op.name
            new_dequantize_op.set_name(f"{new_dequantize_var_name}__post__dequant")
            new_last_op.set_name(f"{last_op_var_name}")
            return True

        return False

        last_op: last op of a valid pattern.
                 E.g. in `conv` -> `relu`, last_op is `relu`; in `conv`, last_op is `conv`.
        _child_op: the child op of the last_op.
        block: current block.
        visited_ops: a dict

        Pattern:
            Given:
                           |-> child_op_1
                last_op -> |-> child_op_2
                           |-> ...
            Result:
                                                     |-> child_op_1
                last_op -> quantize -> dequantize -> |-> child_op_2
                                                     |-> ...
        
coremltools/converters/mil/mil/passes/defs/optimize_activation_quantization.py