Skip to content

Commit

Permalink
Merge pull request #971 from UC-Davis-molecular-computing/588-export-…
Browse files Browse the repository at this point in the history
…to-oxview-format

closes #588: export to oxView format
  • Loading branch information
dave-doty authored Mar 28, 2024
2 parents 6dc78a6 + 4e75ec2 commit c8e5792
Show file tree
Hide file tree
Showing 8 changed files with 278 additions and 23 deletions.
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

0 comments on commit c8e5792

Please sign in to comment.