Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

closes #588: export to oxView format #971

Merged
merged 1 commit into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions lib/src/actions/actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4523,3 +4523,40 @@ abstract class OxdnaExport
@memoized
int get hashCode;
}

abstract class OxviewExport
with BuiltJsonSerializable
implements Action, Built<OxviewExport, OxviewExportBuilder> {
bool get selected_strands_only;

/************************ begin BuiltValue boilerplate ************************/
factory OxviewExport({bool selected_strands_only = false}) {
return OxviewExport.from((b) => b..selected_strands_only = selected_strands_only);
}

OxviewExport._();

factory OxviewExport.from([void Function(OxviewExportBuilder) updates]) = _$OxviewExport;

static Serializer<OxviewExport> get serializer => _$oxviewExportSerializer;

@memoized
int get hashCode;
}

abstract class OxExportOnlySelectedStrandsSet
with BuiltJsonSerializable
implements Action, Built<OxExportOnlySelectedStrandsSet, OxExportOnlySelectedStrandsSetBuilder> {
bool get only_selected;

/************************ begin BuiltValue boilerplate ************************/
factory OxExportOnlySelectedStrandsSet({bool only_selected}) = _$OxExportOnlySelectedStrandsSet._;

OxExportOnlySelectedStrandsSet._();

static Serializer<OxExportOnlySelectedStrandsSet> get serializer =>
_$oxExportOnlySelectedStrandsSetSerializer;

@memoized
int get hashCode;
}
166 changes: 156 additions & 10 deletions lib/src/middleware/oxdna_export.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'dart:convert';
import 'dart:html';
import 'dart:math';
import 'package:path/path.dart' as path;
import 'package:quiver/iterables.dart' as quiver;

import 'package:redux/redux.dart';
import 'package:scadnano/src/state/design.dart';
Expand All @@ -16,9 +18,11 @@ import '../state/app_state.dart';
import '../actions/actions.dart' as actions;
import '../state/helix.dart';
import '../util.dart' as util;
import 'export_cadnano_or_codenano_file.dart' as export_cadnano;
import '../constants.dart' as constants;

oxdna_export_middleware(Store<AppState> store, dynamic action, NextDispatcher next) {
if (action is actions.OxdnaExport) {
if (action is actions.OxdnaExport || action is actions.OxviewExport) {
AppState state = store.state;

List<Strand> strands_to_export;
Expand All @@ -34,18 +38,160 @@ First select some strands, or choose Export🡒oxDNA to export all strands in th
strands_to_export = state.design.strands.toList();
}

Tuple2<String, String> dat_top = to_oxdna_format(state.design, strands_to_export);
String dat = dat_top.item1;
String top = dat_top.item2;
if (action is actions.OxdnaExport) {
Tuple2<String, String> dat_top = to_oxdna_format(state.design, strands_to_export);
String dat = dat_top.item1;
String top = dat_top.item2;

String default_filename = state.ui_state.loaded_filename;
String default_filename_dat = path.setExtension(default_filename, '.dat');
String default_filename_top = path.setExtension(default_filename, '.top');

util.save_file(default_filename_dat, dat);
util.save_file(default_filename_top, top);
} else if (action is actions.OxviewExport) {
String content = to_oxview_format(state.design, strands_to_export);
String default_filename = state.ui_state.loaded_filename;
String default_filename_ext = path.setExtension(default_filename, '.oxview');
util.save_file(default_filename_ext, content);
}
}
next(action);
}

String default_filename = state.ui_state.loaded_filename;
String default_filename_dat = path.setExtension(default_filename, '.dat');
String default_filename_top = path.setExtension(default_filename, '.top');
String to_oxview_format(Design design, List<Strand> strands_to_export) {
OxdnaSystem system = convert_design_to_oxdna_system(design, strands_to_export);
List<Map<String, dynamic>> oxview_strands = [];
int nuc_count = 0;
int strand_count = 0;
List<int> strand_nuc_start = [-1];
assert(strands_to_export.length == system.strands.length);

for (int i = 0; i < strands_to_export.length; i++) {
Strand sc_strand = strands_to_export[i];
OxdnaStrand oxdna_strand = system.strands[i];

strand_count += 1;
List<Map<String, dynamic>> oxvnucs = [];
strand_nuc_start.add(nuc_count);
Map<String, dynamic> oxvstrand = {
'id': strand_count,
'class': 'NucleicAcidStrand',
'end5': nuc_count,
'end3': nuc_count + system.strands[i].nucleotides.length,
'monomers': oxvnucs
};

int scolor;
if (sc_strand.color != null) {
scolor = export_cadnano.to_cadnano_v2_int_hex(sc_strand.color);
} else {
scolor = null;
}

util.save_file(default_filename_dat, dat);
util.save_file(default_filename_top, top);
for (int index_in_strand = 0; index_in_strand < oxdna_strand.nucleotides.length; index_in_strand++) {
OxdnaNucleotide nuc = oxdna_strand.nucleotides[index_in_strand];
Map<String, dynamic> oxvnuc = {
'id': nuc_count,
'p': [nuc.r.x, nuc.r.y, nuc.r.z],
'a1': [nuc.b.x, nuc.b.y, nuc.b.z],
'a3': [nuc.n.x, nuc.n.y, nuc.n.z],
'class': 'DNA',
'type': nuc.base,
'cluster': 1,
};
if (index_in_strand != 0) {
oxvnuc['n5'] = nuc_count - 1;
}
if (index_in_strand != oxdna_strand.nucleotides.length - 1) {
oxvnuc['n3'] = nuc_count + 1;
}
if (scolor != null) {
oxvnuc['color'] = scolor;
}
nuc_count += 1;
oxvnucs.add(oxvnuc);
}
oxview_strands.add(oxvstrand);
}
next(action);

for (int si1 = 0; si1 < strands_to_export.length; si1++) {
Strand sc_strand1 = strands_to_export[si1];
Map<String, dynamic> oxv_strand1 = oxview_strands[si1];
for (int si2 = 0; si2 < strands_to_export.length; si2++) {
Strand sc_strand2 = strands_to_export[si2];
if (!sc_strand1.overlaps(sc_strand2)) {
continue;
}
int s1_nuc_idx = strand_nuc_start[si1 + 1];
for (var domain1 in sc_strand1.domains) {
if (domain1 is Loopout || domain1 is Extension) {
continue;
}
int s2_nuc_idx = strand_nuc_start[si2 + 1];
for (var domain2 in sc_strand2.domains) {
if (domain2 is Loopout || domain2 is Extension) {
continue;
}
if (!domain1.overlaps(domain2)) {
continue;
}
Tuple2<int, int> overlap = domain1.compute_overlap(domain2);
int overlap_left = overlap.item1;
int overlap_right = overlap.item2;
int s1_left = sc_strand1.domain_offset_to_strand_dna_idx(domain1, overlap_left, false);
int s1_right = sc_strand1.domain_offset_to_strand_dna_idx(domain1, overlap_right, false);
int s2_left = sc_strand2.domain_offset_to_strand_dna_idx(domain2, overlap_left, false);
int s2_right = sc_strand2.domain_offset_to_strand_dna_idx(domain2, overlap_right, false);
List<int> d1range;
List<int> d2range;
if (domain1.forward) {
d1range = List<int>.from(quiver.range(s1_left, s1_right));
d2range = List<int>.from(quiver.range(s2_left, s2_right, -1));
} else {
d1range = List<int>.from(quiver.range(s1_right + 1, s1_left + 1));
d2range = List<int>.from(quiver.range(s2_right - 1, s2_left - 1, -1));
}
assert(d1range.length == d2range.length);

// Check for mismatches, and do not add a pair if the bases are *known*
// to mismatch. (FIXME: this must be changed if scadnano later supports
// degenerate base codes.)
for (int i = 0; i < d1range.length; i++) {
int d1 = d1range[i];
int d2 = d2range[i];
if (sc_strand1.dna_sequence != null &&
sc_strand2.dna_sequence != null &&
sc_strand1.dna_sequence[d1] != constants.DNA_BASE_WILDCARD &&
sc_strand2.dna_sequence[d2] != constants.DNA_BASE_WILDCARD &&
util.wc(sc_strand1.dna_sequence[d1]) != sc_strand2.dna_sequence[d2]) {
continue;
}
oxv_strand1['monomers'][d1]['bp'] = s2_nuc_idx + d2;
if (oxview_strands[si2]['monomers'][d2].containsKey('bp')) {
if (oxview_strands[si2]['monomers'][d2]['bp'] != s1_nuc_idx + d1) {
print('${s2_nuc_idx + d2} ${s1_nuc_idx + d1} '
'${oxview_strands[si2]['monomers'][d2]['bp']} ${domain1} ${domain2}');
}
}
}
}
}
}
}
var b = system.compute_bounding_box();
Map<String, dynamic> oxvsystem = {
'box': [b.x, b.y, b.z],
'date': DateTime.now().toIso8601String(),
'systems': [
{'id': 0, 'strands': oxview_strands}
],
'forces': [],
'selections': [],
};
String content = jsonEncode(oxvsystem);

return content;
}

Tuple2<String, String> to_oxdna_format(Design design, [List<Strand> strands_to_export = null]) {
Expand Down
4 changes: 4 additions & 0 deletions lib/src/reducers/app_ui_state_reducer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@ bool show_base_pair_lines_with_mismatches_reducer(
bool export_svg_text_separately_reducer(bool _, actions.ExportSvgTextSeparatelySet action) =>
action.export_svg_text_separately;

bool ox_export_only_selected_strands_reducer(bool _, actions.OxExportOnlySelectedStrandsSet action) =>
action.only_selected;

bool display_major_tick_widths_reducer(bool _, actions.SetDisplayMajorTickWidths action) => action.show;

bool strand_paste_keep_color_reducer(bool _, actions.StrandPasteKeepColorSet action) => action.keep;
Expand Down Expand Up @@ -460,6 +463,7 @@ AppUIStateStorables app_ui_state_storable_local_reducer(AppUIStateStorables stor
..show_base_pair_lines = TypedReducer<bool, actions.ShowBasePairLinesSet>(show_base_pair_lines_reducer)(storables.show_base_pair_lines, action)
..show_base_pair_lines_with_mismatches = TypedReducer<bool, actions.ShowBasePairLinesWithMismatchesSet>(show_base_pair_lines_with_mismatches_reducer)(storables.show_base_pair_lines_with_mismatches, action)
..export_svg_text_separately = TypedReducer<bool, actions.ExportSvgTextSeparatelySet>(export_svg_text_separately_reducer)(storables.export_svg_text_separately, action)
..ox_export_only_selected_strands = TypedReducer<bool, actions.OxExportOnlySelectedStrandsSet>(ox_export_only_selected_strands_reducer)(storables.ox_export_only_selected_strands, action)
..only_display_selected_helices = TypedReducer<bool, actions.SetOnlyDisplaySelectedHelices>(only_display_selected_helices_reducer)(storables.only_display_selected_helices, action)
..default_crossover_type_scaffold_for_setting_helix_rolls = TypedReducer<bool, actions.DefaultCrossoverTypeForSettingHelixRollsSet>(default_crossover_type_scaffold_for_setting_helix_rolls_reducer)(storables.default_crossover_type_scaffold_for_setting_helix_rolls, action)
..default_crossover_type_staple_for_setting_helix_rolls = TypedReducer<bool, actions.DefaultCrossoverTypeForSettingHelixRollsSet>(default_crossover_type_staple_for_setting_helix_rolls_reducer)(storables.default_crossover_type_staple_for_setting_helix_rolls, action)
Expand Down
2 changes: 2 additions & 0 deletions lib/src/serializers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,8 @@ part 'serializers.g.dart';
ZoomSpeedSet,
NewDesignSet,
OxdnaExport,
OxviewExport,
OxExportOnlySelectedStrandsSet,
Design,
AssignDomainNameComplementFromBoundStrands,
AssignDomainNameComplementFromBoundDomains,
Expand Down
2 changes: 2 additions & 0 deletions lib/src/state/app_ui_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ abstract class AppUIState with BuiltJsonSerializable implements Built<AppUIState

bool get selection_box_intersection => storables.selection_box_intersection;

bool get ox_export_only_selected_strands => storables.ox_export_only_selected_strands;

static void _initializeBuilder(AppUIStateBuilder b) {
b.copy_info = null;
b.last_mod_5p = null;
Expand Down
3 changes: 3 additions & 0 deletions lib/src/state/app_ui_state_storables.dart
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ abstract class AppUIStateStorables

bool get export_svg_text_separately;

bool get ox_export_only_selected_strands;

static void _initializeBuilder(AppUIStateStorablesBuilder b) {
// This ensures that even if these keys are not in localStorage (e.g., due to upgrading),
// then they will be populated with a default value instead of raising an exception.
Expand Down Expand Up @@ -188,6 +190,7 @@ abstract class AppUIStateStorables
b.show_mouseover_data = false;
b.selection_box_intersection = false;
b.export_svg_text_separately = false;
b.ox_export_only_selected_strands = false;
}

/************************ begin BuiltValue boilerplate ************************/
Expand Down
53 changes: 53 additions & 0 deletions lib/src/state/strand.dart
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,59 @@ abstract class Strand
return rebuild((strand) => strand..substrands.replace(substrands_new));
}

/// Convert from offset on the given Domain's Helix to string index on the parent Strand's DNA sequence.
/// If `offset_closer_to_5p` is ``true``, (this only matters if `offset` contains an insertion)
/// then the only leftmost string index corresponding to this offset is included,
/// otherwise up to the rightmost string index (including all insertions) is included.
int domain_offset_to_strand_dna_idx(Domain domain, int offset, bool offset_closer_to_5p) {
if (domain.deletions.contains(offset)) {
throw ArgumentError('offset ${offset} illegally contains a deletion from ${domain.deletions}');
}

int len_adjust = this._net_ins_del_length_increase_from_5p_to(domain, offset, offset_closer_to_5p);

int domain_str_idx;
if (domain.forward) {
offset += len_adjust;
domain_str_idx = offset - domain.start;
} else {
offset -= len_adjust;
domain_str_idx = domain.end - 1 - offset;
}

return domain_str_idx + get_seq_start_idx(domain);
}

/// Net number of insertions from 5'/3' end to offset_edge,
/// INCLUSIVE on 5'/3' end, EXCLUSIVE on offset_edge.
/// Set `five_p` ``= False`` to test from 3' end to `offset_edge`.
int _net_ins_del_length_increase_from_5p_to(Domain domain, int offset_edge, bool offset_closer_to_5p) {
int length_increase = 0;
for (int deletion in domain.deletions) {
if (_between_5p_and_offset(domain, deletion, offset_edge)) {
length_increase -= 1;
}
}
for (var insertion in domain.insertions) {
if (_between_5p_and_offset(domain, insertion.offset, offset_edge)) {
length_increase += insertion.length;
}
}
if (!offset_closer_to_5p) {
Map<int, int> insertion_map =
Map<int, int>.fromIterable(domain.insertions, key: (e) => e.offset, value: (e) => e.length);
if (insertion_map.containsKey(offset_edge)) {
int insertion_length = insertion_map[offset_edge];
length_increase += insertion_length;
}
}
return length_increase;
}

bool _between_5p_and_offset(Domain domain, int offset_to_test, int offset_edge) =>
(domain.forward && domain.start <= offset_to_test && offset_to_test < offset_edge) ||
(!domain.forward && offset_edge < offset_to_test && offset_to_test < domain.end);

String _trim_or_pad_sequence_to_desired_length(String dna_sequence_new, int desired_length) {
// truncate dna_sequence_new if too long; pad with ?'s if to short
int seq_len = dna_sequence_new.length;
Expand Down
Loading
Loading